<?xml version="1.0" encoding="UTF-8"?><rss version="2.0" xmlns:content="http://purl.org/rss/1.0/modules/content/"><channel><title>A Craftsman</title><description>tracerZzz&apos;blog</description><link>https://tracerzzz.cn/</link><templateTheme>Firefly</templateTheme><templateThemeVersion>6.16.8</templateThemeVersion><templateThemeUrl>https://github.com/CuteLeaf/Firefly</templateThemeUrl><lastBuildDate>2026年10月31日 21:23:09</lastBuildDate><item><title>一家多诊室的医院：用类比理解 Go 的 GMP 调度</title><link>https://tracerzzz.cn/posts/go-gmp-scheduler/</link><guid isPermaLink="true">https://tracerzzz.cn/posts/go-gmp-scheduler/</guid><description>用&quot;医院看病&quot;的类比讲清楚 Go 的 GMP 调度：G、M、P、全局队列、偷任务、抢占、系统调用、netpoller 和 sysmon 各自做什么，顺带回顾用户态和内核态，对比 Python asyncio、Node.js 和 Go 的调度分层，最后用两个小程序亲眼看看调度器在干什么。</description><pubDate>Fri, 30 Oct 2026 16:40:00 GMT</pubDate><content:encoded>&lt;p&gt;&lt;/p&gt;&lt;figure&gt;&lt;img alt=&quot;一家多诊室的医院：四间诊室 P0 到 P3 各有一位医生，P3 门口没人，正从 P2 门口偷走一半病人；下面的大厅是全局队列，角落里的巡查员是 sysmon&quot; loading=&quot;lazy&quot; width=&quot;1920&quot; height=&quot;1080&quot; src=&quot;/_astro/hospital.CbnWzOEF_29WGbp.webp&quot; /&gt;&lt;figcaption&gt;一家多诊室的医院：四间诊室 P0 到 P3 各有一位医生，P3 门口没人，正从 P2 门口偷走一半病人；下面的大厅是全局队列，角落里的巡查员是 sysmon&lt;/figcaption&gt;&lt;/figure&gt;&lt;p&gt;&lt;/p&gt;
&lt;p&gt;我之前在&lt;a href=&quot;/posts/python-asyncio-concurrent-queries/&quot;&gt;用 Python 协程把成本数据产出提速十倍&lt;/a&gt;那篇里，用”医院看病”讲过 Python 的异步：一个医生、一个诊室、病人去做检查时医生先看下一位。这次就沿着这个类比往下走：&lt;strong&gt;Go 相当于把这家诊所扩建成了一家多诊室的医院&lt;/strong&gt;。&lt;/p&gt;
&lt;blockquote&gt;&lt;p&gt;说明：文中涉及的运行时细节，我对照了 Go 1.24 的运行时源码（&lt;code&gt;runtime/proc.go&lt;/code&gt; 等）；两个示例程序是在 Go 1.24 上实际跑过的。不同版本的实现细节可能有出入。&lt;/p&gt;&lt;/blockquote&gt;
&lt;section&gt;&lt;h2&gt;一、整体印象：一个进程，多个线程&lt;a href=&quot;#一整体印象一个进程多个线程&quot;&gt;&lt;span&gt;#&lt;/span&gt;&lt;/a&gt;&lt;/h2&gt;&lt;p&gt;先把最重要的结构定下来：&lt;strong&gt;一个 Go 程序只有一个进程，G、M、P 全部都在这一个进程里&lt;/strong&gt;。&lt;/p&gt;&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;G&lt;/strong&gt; 是 goroutine，数量可以有几十万个；&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;M&lt;/strong&gt; 是操作系统线程，一个进程里有多个；&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;P&lt;/strong&gt; 是逻辑处理器，只是这个进程内存里的一个数据结构，数量默认等于 CPU 核数；&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;全局队列&lt;/strong&gt;是调度器里的一个队列，所有线程都可能访问它，所以要加锁。&lt;/li&gt;
&lt;/ul&gt;&lt;p&gt;P 之间、M 之间要协作，靠的是直接读写同一个进程里的共享内存，配合锁和原子操作，不涉及进程间通信，也没有主进程、子进程之分。&lt;/p&gt;&lt;/section&gt;
&lt;section&gt;&lt;h2&gt;二、用户态和内核态&lt;a href=&quot;#二用户态和内核态&quot;&gt;&lt;span&gt;#&lt;/span&gt;&lt;/a&gt;&lt;/h2&gt;&lt;p&gt;要理解 goroutine 为什么”轻”，得先回到操作系统。&lt;/p&gt;&lt;p&gt;CPU 有权限等级，内核跑在最高权限，普通程序跑在最低权限。程序不能直接操作硬件、不能碰别的进程的内存，凡是读写文件、收发网络包、创建线程这类事情，都要通过&lt;strong&gt;系统调用&lt;/strong&gt;请内核来做。进入内核的方式有三种：&lt;/p&gt;&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;系统调用&lt;/strong&gt;：程序主动请求，比如 &lt;code&gt;read&lt;/code&gt;、&lt;code&gt;write&lt;/code&gt;、&lt;code&gt;epoll_wait&lt;/code&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;/ul&gt;&lt;p&gt;进出内核要保存寄存器、切换栈、做权限检查，还会让 CPU 缓存变”冷”。更贵的是&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;th&gt;goroutine 切换&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;Go 运行时，在用户态完成&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;百纳秒级&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;栈&lt;/td&gt;&lt;td&gt;Linux 默认按 8MB 预留&lt;/td&gt;&lt;td&gt;初始 2KB，按需增长&lt;/td&gt;&lt;/tr&gt;&lt;/tbody&gt;&lt;/table&gt;&lt;p&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;内核态是后勤区&lt;/strong&gt;：药房、档案室、检验科、收发室、人事科。医生不能自己进去，只能到窗口填单子，由院方办理；&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;线程切换&lt;/strong&gt;是人事科给医生换岗：交接、换衣服，要经过人事科；&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;goroutine 切换&lt;/strong&gt;是同一个医生换下一个病人：放下这份病历夹，拿起下一份，在诊室里自己就能完成。&lt;/li&gt;
&lt;/ul&gt;&lt;p&gt;Go 调度器很多设计的出发点，就是&lt;strong&gt;尽量少让医生去后勤窗口干等&lt;/strong&gt;。&lt;/p&gt;&lt;/section&gt;
&lt;section&gt;&lt;h2&gt;三、G、M、P 三个角色&lt;a href=&quot;#三gmp-三个角色&quot;&gt;&lt;span&gt;#&lt;/span&gt;&lt;/a&gt;&lt;/h2&gt;

&lt;table&gt;&lt;thead&gt;&lt;tr&gt;&lt;th&gt;Go&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;G（goroutine）&lt;/td&gt;&lt;td&gt;病人，手里拿着自己的病历夹&lt;/td&gt;&lt;td&gt;病历夹里记着看到哪一步了，就是 G 的栈和上下文&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;M（machine，操作系统线程）&lt;/td&gt;&lt;td&gt;医生&lt;/td&gt;&lt;td&gt;真正执行代码的永远是线程&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;P（processor，逻辑处理器）&lt;/td&gt;&lt;td&gt;诊室&lt;/td&gt;&lt;td&gt;医生必须进了诊室才能看病&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;P 的本地队列&lt;/td&gt;&lt;td&gt;诊室门口的候诊椅&lt;/td&gt;&lt;td&gt;最多 256 把椅子，只有本诊室叫号，基本不用加锁&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;P 的 &lt;code&gt;runnext&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;/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;整家医院运转起来是这样的，每个地方都标上了对应的 Go 模块，后面几节会逐个展开：&lt;/p&gt;&lt;p&gt;&lt;/p&gt;&lt;figure&gt;&lt;img alt=&quot;医院运转图：门诊区是用户态，诊室是 P，里面有优先位 runnext、候诊椅本地队列和医生 M；大厅是全局队列，休息室是空闲的 M，值班医生是阻塞在 epoll_wait 的 M，巡查员是 sysmon；后勤区是内核态，检验科是 epoll，档案室是阻塞的系统调用，人事科是内核的线程调度&quot; loading=&quot;lazy&quot; width=&quot;960&quot; height=&quot;680&quot; src=&quot;/_astro/gmp-hospital.DVDzbKOB_Z18gWuo.svg&quot; /&gt;&lt;figcaption&gt;医院运转图：门诊区是用户态，诊室是 P，里面有优先位 runnext、候诊椅本地队列和医生 M；大厅是全局队列，休息室是空闲的 M，值班医生是阻塞在 epoll_wait 的 M，巡查员是 sysmon；后勤区是内核态，检验科是 epoll，档案室是阻塞的系统调用，人事科是内核的线程调度&lt;/figcaption&gt;&lt;/figure&gt;&lt;p&gt;&lt;/p&gt;&lt;p&gt;几个要点：&lt;/p&gt;&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;P 的数量默认等于 CPU 核数&lt;/strong&gt;（&lt;code&gt;GOMAXPROCS&lt;/code&gt;）。4 核就是 4 间诊室，同一时刻最多 4 个医生在看病，正好把 4 个核用满。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;P 不绑定某个具体的核&lt;/strong&gt;。P 更像一张”允许执行 Go 代码的通行证”，线程拿到 P 才能跑 goroutine；至于这个线程跑在哪个核上，是操作系统决定的。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;M 可以比 P 多&lt;/strong&gt;。有的医生被拉去后勤窗口等着了（阻塞在系统调用里），诊室会交给别的医生，所以医生总数会超过诊室数。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;P 自己不”做”任何事&lt;/strong&gt;。它只是存着队列和一些缓存，真正执行代码的是 M。准确的说法是”M 拿着 P 去找 G”。&lt;/li&gt;
&lt;/ul&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;&lt;p&gt;一个 Go 进程&lt;/p&gt;&lt;/span&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;&lt;/span&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;&lt;/span&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;&lt;/span&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;&lt;/span&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;&lt;/span&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;&lt;/span&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;&lt;/span&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;&lt;/span&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;&lt;/span&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;&lt;p&gt;P2&lt;/p&gt;&lt;/span&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;&lt;p&gt;runnext: 空&lt;/p&gt;&lt;/span&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;&lt;p&gt;本地队列: 空&lt;/p&gt;&lt;/span&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;&lt;p&gt;P1&lt;/p&gt;&lt;/span&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;&lt;p&gt;runnext: 空&lt;/p&gt;&lt;/span&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;&lt;p&gt;本地队列: G4&lt;/p&gt;&lt;/span&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;&lt;p&gt;P0&lt;/p&gt;&lt;/span&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;&lt;p&gt;runnext: G9&lt;/p&gt;&lt;/span&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;&lt;p&gt;本地队列: G1 G2 G3&lt;/p&gt;&lt;/span&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;&lt;p&gt;全局队列（加锁访问）&lt;/p&gt;&lt;/span&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;&lt;p&gt;M0 线程：正在跑 G7&lt;/p&gt;&lt;/span&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;&lt;p&gt;M1 线程：正在跑 G8&lt;/p&gt;&lt;/span&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;&lt;p&gt;M2 线程：没活了，准备去偷&lt;/p&gt;&lt;/span&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;&lt;p&gt;操作系统：把线程调度到 CPU 核上&lt;/p&gt;&lt;/span&gt;&lt;/div&gt;
&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;&lt;p&gt;一个 Go 进程&lt;/p&gt;&lt;/span&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;&lt;/span&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;&lt;/span&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;&lt;/span&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;&lt;/span&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;&lt;/span&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;&lt;/span&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;&lt;/span&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;&lt;/span&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;&lt;/span&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;&lt;p&gt;P2&lt;/p&gt;&lt;/span&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;&lt;p&gt;runnext: 空&lt;/p&gt;&lt;/span&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;&lt;p&gt;本地队列: 空&lt;/p&gt;&lt;/span&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;&lt;p&gt;P1&lt;/p&gt;&lt;/span&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;&lt;p&gt;runnext: 空&lt;/p&gt;&lt;/span&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;&lt;p&gt;本地队列: G4&lt;/p&gt;&lt;/span&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;&lt;p&gt;P0&lt;/p&gt;&lt;/span&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;&lt;p&gt;runnext: G9&lt;/p&gt;&lt;/span&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;&lt;p&gt;本地队列: G1 G2 G3&lt;/p&gt;&lt;/span&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;&lt;p&gt;全局队列（加锁访问）&lt;/p&gt;&lt;/span&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;&lt;p&gt;M0 线程：正在跑 G7&lt;/p&gt;&lt;/span&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;&lt;p&gt;M1 线程：正在跑 G8&lt;/p&gt;&lt;/span&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;&lt;p&gt;M2 线程：没活了，准备去偷&lt;/p&gt;&lt;/span&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;&lt;p&gt;操作系统：把线程调度到 CPU 核上&lt;/p&gt;&lt;/span&gt;&lt;/div&gt;
&lt;/div&gt;&lt;/div&gt;&lt;/div&gt;&lt;/section&gt;
&lt;section&gt;&lt;h2&gt;四、新建的 G 放在哪&lt;a href=&quot;#四新建的-g-放在哪&quot;&gt;&lt;span&gt;#&lt;/span&gt;&lt;/a&gt;&lt;/h2&gt;&lt;p&gt;&lt;code&gt;go f()&lt;/code&gt; 创建的 G，放进&lt;strong&gt;当前这个 P&lt;/strong&gt; 里：&lt;/p&gt;&lt;ol&gt;
&lt;li&gt;先放进 &lt;code&gt;runnext&lt;/code&gt;，原来占着 &lt;code&gt;runnext&lt;/code&gt; 的那个 G 被挤到本地队列尾部；&lt;/li&gt;
&lt;li&gt;本地队列最多 256 个，满了就把一半（128 个）连同新的这个一起挪到全局队列。&lt;/li&gt;
&lt;/ol&gt;&lt;p&gt;挪到全局队列后，&lt;strong&gt;不会再主动分给别的 P&lt;/strong&gt;，而是别的 P 没活时自己来拿，或者直接去偷。&lt;/p&gt;&lt;p&gt;&lt;strong&gt;goroutine 是先进先出，还是后加入的先执行？&lt;/strong&gt;&lt;/p&gt;&lt;p&gt;要分清两样东西。每个 G 自己的&lt;strong&gt;调用栈&lt;/strong&gt;当然是后进先出，和任何语言一样。但&lt;strong&gt;等待执行的队列&lt;/strong&gt;，本地队列和全局队列都是先进先出。唯一的例外是 &lt;code&gt;runnext&lt;/code&gt;：&lt;strong&gt;最新创建的那一个 G 会插队&lt;/strong&gt;，下一个就执行它。&lt;/p&gt;&lt;p&gt;为什么要让它插队？刚创建的 G 往往和当前 G 关系密切，比如刚往 channel 里发了数据，新起的 G 马上要收，立刻执行能用上还热着的 CPU 缓存。为了防止两个 G 互相通过 &lt;code&gt;runnext&lt;/code&gt; 插队、把队列里其他 G 饿死，插队的 G 会继承当前 G 剩下的时间片，而不是拿一个新的。&lt;/p&gt;&lt;p&gt;用医院说：病人挂号后坐到挂号诊室门口；刚转过来的病人坐优先位，下一个就看；门口坐满了，一半人被送去大厅。&lt;/p&gt;&lt;/section&gt;
&lt;section&gt;&lt;h2&gt;五、M 怎么找下一个 G&lt;a href=&quot;#五m-怎么找下一个-g&quot;&gt;&lt;span&gt;#&lt;/span&gt;&lt;/a&gt;&lt;/h2&gt;&lt;p&gt;当前的 G 主动让出（等 channel、等锁、等网络）、被抢占，或者执行结束时，M 就进入运行时的调度函数，按固定顺序找下一个 G：&lt;/p&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;&lt;/span&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;&lt;/span&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;&lt;/span&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;&lt;/span&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;&lt;/span&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;&lt;/span&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;&lt;p&gt;找到了&lt;/p&gt;&lt;/span&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;&lt;p&gt;找到了&lt;/p&gt;&lt;/span&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;&lt;p&gt;找到了&lt;/p&gt;&lt;/span&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;&lt;p&gt;找到了&lt;/p&gt;&lt;/span&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;&lt;p&gt;找到了&lt;/p&gt;&lt;/span&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;&lt;p&gt;找到了&lt;/p&gt;&lt;/span&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;1. 每调度 61 次，先从全局队列拿一个&lt;/span&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;2. 看自己 P 的 runnext&lt;/span&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;3. 看自己 P 的本地队列&lt;/span&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;4. 看全局队列（加锁，顺便多拿一批回本地）&lt;/span&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;5. 非阻塞地查一下网络轮询器&lt;/span&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;6. 随机顺序去别的 P 偷一半&lt;/span&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;7. 都没有：交出 P，M 休眠&lt;/span&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;&lt;p&gt;在当前 M 上执行这个 G&lt;br /&gt;&lt;br /&gt;让出或被抢占后，回到第 1 步&lt;/p&gt;&lt;/span&gt;&lt;/div&gt;
&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;&lt;/span&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;&lt;/span&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;&lt;/span&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;&lt;/span&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;&lt;/span&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;&lt;/span&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;&lt;p&gt;找到了&lt;/p&gt;&lt;/span&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;&lt;p&gt;找到了&lt;/p&gt;&lt;/span&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;&lt;p&gt;找到了&lt;/p&gt;&lt;/span&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;&lt;p&gt;找到了&lt;/p&gt;&lt;/span&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;&lt;p&gt;找到了&lt;/p&gt;&lt;/span&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;&lt;p&gt;找到了&lt;/p&gt;&lt;/span&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;1. 每调度 61 次，先从全局队列拿一个&lt;/span&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;2. 看自己 P 的 runnext&lt;/span&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;3. 看自己 P 的本地队列&lt;/span&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;4. 看全局队列（加锁，顺便多拿一批回本地）&lt;/span&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;5. 非阻塞地查一下网络轮询器&lt;/span&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;6. 随机顺序去别的 P 偷一半&lt;/span&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;7. 都没有：交出 P，M 休眠&lt;/span&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;&lt;p&gt;在当前 M 上执行这个 G&lt;br /&gt;&lt;br /&gt;让出或被抢占后，回到第 1 步&lt;/p&gt;&lt;/span&gt;&lt;/div&gt;
&lt;/div&gt;&lt;/div&gt;&lt;/div&gt;&lt;p&gt;越靠前的越便宜：本地队列基本不用加锁，全局队列要加锁，去偷别人的最贵。&lt;/p&gt;&lt;p&gt;&lt;strong&gt;这个循环是每个 M 自己转的&lt;/strong&gt;。没有一个”总调度员”线程在给大家分活，4 个 P 就有 4 个 M 各自转这个循环，只在访问全局队列或者偷别人时才有交集。“调度器”指的是这套规则和共享的数据结构，而不是一个单独的线程。用医院说，就是&lt;strong&gt;每个医生看完一个病人，自己去找下一个&lt;/strong&gt;。&lt;/p&gt;&lt;section&gt;&lt;h3&gt;为什么每 61 次要先查全局队列&lt;a href=&quot;#为什么每-61-次要先查全局队列&quot;&gt;&lt;span&gt;#&lt;/span&gt;&lt;/a&gt;&lt;/h3&gt;&lt;p&gt;M 平时优先看本地队列。如果本地队列一直有活，比如两个 G 不停地互相创建新的 G，M 就永远轮不到全局队列，那里的 G 会被饿死。运行时源码里的注释说的正是这个情况。所以每调度 61 次，强制先从全局队列拿一个。&lt;/p&gt;&lt;p&gt;为什么是 61：不能太频繁，全局队列要加锁；也不能太稀疏，否则全局队列等得太久。61 是个质数，不容易和程序里某种固定周期的行为撞在一起。&lt;/p&gt;&lt;p&gt;医院里就是：医生每看 61 个门口的病人，去大厅叫一次号。&lt;/p&gt;&lt;/section&gt;&lt;section&gt;&lt;h3&gt;偷任务是怎么偷的&lt;a href=&quot;#偷任务是怎么偷的&quot;&gt;&lt;span&gt;#&lt;/span&gt;&lt;/a&gt;&lt;/h3&gt;&lt;p&gt;M 走到第 6 步时：&lt;/p&gt;&lt;ol&gt;
&lt;li&gt;把所有 P 按随机顺序排一遍，避免一群空闲的 M 同时去抢同一个 P；&lt;/li&gt;
&lt;li&gt;从目标 P 的本地队列里拿走&lt;strong&gt;一半&lt;/strong&gt;（有 3 个就拿 2 个），靠原子操作修改对方队列的头指针完成，不用加锁，对方照常工作；&lt;/li&gt;
&lt;li&gt;偷到的 G 放进自己的本地队列，马上执行其中一个；&lt;/li&gt;
&lt;li&gt;一轮没偷到就再来，最多 4 轮，最后一轮还会尝试偷对方的 &lt;code&gt;runnext&lt;/code&gt;。&lt;/li&gt;
&lt;/ol&gt;&lt;p&gt;为什么是一半：偷一个，过一会儿又空了，又得来偷；偷全部，对方空了，换成对方来偷你。一半能让两边大致平衡。&lt;/p&gt;&lt;p&gt;另外，同时在”找活”的 M 数量有上限，防止一大堆空闲线程空转抢 CPU。&lt;/p&gt;&lt;p&gt;医院里就是：医生门口没人了，先去大厅看看，再问检验科有没有出结果的，还是没有，就去隔壁诊室门口领走一半等着的病人。&lt;/p&gt;&lt;/section&gt;&lt;/section&gt;
&lt;section&gt;&lt;h2&gt;六、抢占：某个病人看得太久&lt;a href=&quot;#六抢占某个病人看得太久&quot;&gt;&lt;span&gt;#&lt;/span&gt;&lt;/a&gt;&lt;/h2&gt;&lt;p&gt;如果一个 G 一直在算，不让出，会不会霸占 P？&lt;/p&gt;&lt;p&gt;后台有个监控线程叫 &lt;strong&gt;sysmon&lt;/strong&gt;，它发现某个 G 连续运行超过 &lt;strong&gt;10ms&lt;/strong&gt;，就会要求它让出：&lt;/p&gt;&lt;ol&gt;
&lt;li&gt;先做个标记，等它自己停。编译器在几乎每个函数开头都插了一段检查栈空间的代码，G 下次调用函数时会看到这个标记，主动进入调度。这是 Go 1.14 以前唯一的办法，缺点是如果循环里根本不调用函数，就永远看不到。&lt;/li&gt;
&lt;li&gt;Go 1.14 起，还会&lt;strong&gt;给这个线程发一个 &lt;code&gt;SIGURG&lt;/code&gt; 信号&lt;/strong&gt;。线程收到信号后，运行时检查当前位置是否安全（GC 能否正确识别栈上哪些是指针、是不是正在执行不能被打断的运行时代码），安全就保存现场、跳进调度器；不安全就下次再试。选 &lt;code&gt;SIGURG&lt;/code&gt; 是因为普通程序几乎不用它。&lt;/li&gt;
&lt;/ol&gt;&lt;p&gt;被抢占的 G 会放进&lt;strong&gt;全局队列&lt;/strong&gt;重新排队，之后轮到它时从被打断的那条指令继续执行，业务代码感知不到。&lt;/p&gt;&lt;p&gt;医院里就是：某个病人看了太久，巡查员敲门提醒，这个病人先拿着病历夹回大厅排队。&lt;/p&gt;&lt;p&gt;&lt;strong&gt;这样做好吗？&lt;/strong&gt; 好处不只是公平：&lt;/p&gt;&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;GC 需要所有 G 停下来&lt;/strong&gt;。GC 的两次短暂停要求所有 goroutine 都到达安全点。在没有异步抢占的年代，一个不调用函数的死循环会让暂停永远等不到它，整个程序卡死；&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;防止饿死、降低延迟&lt;/strong&gt;，计算密集的 G 不会让同一个 P 上排队的请求一直等。&lt;/li&gt;
&lt;/ul&gt;&lt;p&gt;代价：保存和恢复寄存器、缓存变冷（但 10ms 对 CPU 来说已经很长了）；被抢占的 G 要重新排队；信号可能打断正在进行的系统调用，让它返回 &lt;code&gt;EINTR&lt;/code&gt;。标准库内部都做了重试，但调用 C 库或没处理 &lt;code&gt;EINTR&lt;/code&gt; 的代码可能出问题，可以用 &lt;code&gt;GODEBUG=asyncpreemptoff=1&lt;/code&gt; 关掉异步抢占来排查。&lt;/p&gt;&lt;p&gt;另外，抢占只管”轮流用 CPU”，不管数据安全，并发读写共享数据该加锁还得加锁。&lt;/p&gt;&lt;section&gt;&lt;h3&gt;亲眼看看抢占&lt;a href=&quot;#亲眼看看抢占&quot;&gt;&lt;span&gt;#&lt;/span&gt;&lt;/a&gt;&lt;/h3&gt;&lt;p&gt;只留一个 P，起一个不调用任何函数的死循环：&lt;/p&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;&lt;figure&gt;&lt;figcaption&gt;&lt;/figcaption&gt;&lt;pre&gt;&lt;code&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;1&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;package&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;main&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;2&lt;/div&gt;&lt;/div&gt;&lt;div&gt;
&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;3&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;import&lt;/span&gt;&lt;span&gt; (&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;4&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;  &lt;/span&gt;&lt;span&gt;&quot;fmt&quot;&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;5&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;  &lt;/span&gt;&lt;span&gt;&quot;runtime&quot;&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;6&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;  &lt;/span&gt;&lt;span&gt;&quot;time&quot;&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;7&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;)&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;8&lt;/div&gt;&lt;/div&gt;&lt;div&gt;
&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;9&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;func&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;main&lt;/span&gt;&lt;span&gt;() {&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;10&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;  &lt;/span&gt;&lt;span&gt;runtime&lt;/span&gt;&lt;span&gt;.&lt;/span&gt;&lt;span&gt;GOMAXPROCS&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;1&lt;/span&gt;&lt;span&gt;) &lt;/span&gt;&lt;span&gt;// 只留一个 P&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;11&lt;/div&gt;&lt;/div&gt;&lt;div&gt;
&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;12&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;  &lt;/span&gt;&lt;span&gt;go&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;func&lt;/span&gt;&lt;span&gt;() {&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;13&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;    &lt;/span&gt;&lt;span&gt;for&lt;/span&gt;&lt;span&gt; {&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;14&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;&lt;span&gt;    &lt;/span&gt;&lt;/span&gt;&lt;span&gt;} &lt;/span&gt;&lt;span&gt;// 纯计算的死循环，不调用任何函数&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;15&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;&lt;span&gt;  &lt;/span&gt;&lt;/span&gt;&lt;span&gt;}()&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;16&lt;/div&gt;&lt;/div&gt;&lt;div&gt;
&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;17&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;  &lt;/span&gt;&lt;span&gt;time&lt;/span&gt;&lt;span&gt;.&lt;/span&gt;&lt;span&gt;Sleep&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;100&lt;/span&gt;&lt;span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;*&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;/span&gt;&lt;span&gt;time&lt;/span&gt;&lt;span&gt;.&lt;/span&gt;&lt;span&gt;Millisecond&lt;/span&gt;&lt;span&gt;) &lt;/span&gt;&lt;span&gt;// 让出 P，死循环开始跑&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;18&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;  &lt;/span&gt;&lt;span&gt;fmt&lt;/span&gt;&lt;span&gt;.&lt;/span&gt;&lt;span&gt;Println&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;&quot;main 又跑起来了：死循环被抢占了&quot;&lt;/span&gt;&lt;span&gt;)&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;19&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;}&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;/code&gt;&lt;/pre&gt;&lt;div&gt;&lt;div&gt;&lt;/div&gt;&lt;div&gt;&lt;/div&gt;&lt;/div&gt;&lt;/figure&gt;&lt;div&gt;&lt;/div&gt;&lt;/div&gt;&lt;span&gt;展开&lt;/span&gt;&lt;span&gt;收起&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;p&gt;&lt;code&gt;main&lt;/code&gt; 睡眠时让出了唯一的 P，死循环开始独占它。100ms 后 &lt;code&gt;main&lt;/code&gt; 该醒了，但它需要 P 才能执行：&lt;/p&gt;&lt;div&gt;&lt;figure&gt;&lt;figcaption&gt;&lt;span&gt;&lt;/span&gt;&lt;span&gt;Terminal window&lt;/span&gt;&lt;/figcaption&gt;&lt;pre&gt;&lt;code&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;1&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;$&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;go&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;run&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;main.go&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;2&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;main&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;又跑起来了：死循环被抢占了&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;3&lt;/div&gt;&lt;/div&gt;&lt;div&gt;
&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;4&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;$&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;GODEBUG=asyncpreemptoff=&lt;/span&gt;&lt;span&gt;1&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;timeout&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;3&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;go&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;run&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;main.go&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;5&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;$&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;echo&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;$?&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;6&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;124&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;/code&gt;&lt;/pre&gt;&lt;div&gt;&lt;div&gt;&lt;/div&gt;&lt;div&gt;&lt;/div&gt;&lt;/div&gt;&lt;/figure&gt;&lt;/div&gt;&lt;p&gt;默认情况下，死循环被信号抢占，&lt;code&gt;main&lt;/code&gt; 正常打印退出。关掉异步抢占后，死循环里没有任何函数调用，没机会看到抢占标记，&lt;code&gt;main&lt;/code&gt; 永远拿不回 P，直到被 &lt;code&gt;timeout&lt;/code&gt; 杀掉（退出码 124）。这就是 Go 1.14 以前的行为。&lt;/p&gt;&lt;/section&gt;&lt;/section&gt;
&lt;section&gt;&lt;h2&gt;七、系统调用阻塞了怎么办&lt;a href=&quot;#七系统调用阻塞了怎么办&quot;&gt;&lt;span&gt;#&lt;/span&gt;&lt;/a&gt;&lt;/h2&gt;&lt;p&gt;读文件这类系统调用会让线程卡在内核里。这时 G 和 M 是绑在一起的：内核只认识线程，线程卡在内核里，就是在替这个 G 等结果。问题是 M 手上还拿着 P，P 门口的其他 G 怎么办？&lt;/p&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;M2sysmonPM1G（读文件）M2sysmonPM1G（读文件）原来的 P 空闲就拿回否则找任意空闲 P都没有就把 G 放进全局队列，M1 休眠alt[很快返回][卡住了一段时间]发起 read 系统调用标记&quot;系统调用中&quot;，暂时不交出直接拿回 P，接着执行 G发现 P 一直处在系统调用中，且还有 G 在等把 P 交给 M2（没有空闲 M 就新建）继续执行队列里的其他 G系统调用返回，带着 G 找 P
&lt;/div&gt;&lt;div&gt;M2sysmonPM1G（读文件）M2sysmonPM1G（读文件）原来的 P 空闲就拿回否则找任意空闲 P都没有就把 G 放进全局队列，M1 休眠alt[很快返回][卡住了一段时间]发起 read 系统调用标记&quot;系统调用中&quot;，暂时不交出直接拿回 P，接着执行 G发现 P 一直处在系统调用中，且还有 G 在等把 P 交给 M2（没有空闲 M 就新建）继续执行队列里的其他 G系统调用返回，带着 G 找 P
&lt;/div&gt;&lt;/div&gt;&lt;/div&gt;&lt;p&gt;几个细节：&lt;/p&gt;&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;不急着交出 P&lt;/strong&gt;。大多数系统调用很快就返回，每次都转交，开销反而更大。对已知一定会长时间阻塞的调用，运行时会在进入时就直接交出。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;为什么拿不回原来的 P&lt;/strong&gt;：卡住的这段时间里，P 已经被转交给别的 M 在用了，这正是转交的目的。转交出去就是别人的，没有”用完还给原主”的约定。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;结果属于 G，不属于 M&lt;/strong&gt;。系统调用返回的数据写在 G 自己的栈和上下文里，M 只是执行者，不保存业务状态。所以 G 放进全局队列后，任何一个 M 取到它，都能从系统调用的下一行继续，业务代码感知不到中间换了线程。一个 goroutine 一生中可能在好几个线程上运行过。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;为什么不让 M 原地等 P&lt;/strong&gt;：等待本身也要占一个线程，而 G 放进全局队列后谁都能执行它，效果一样，还更省资源。&lt;/li&gt;
&lt;/ul&gt;&lt;p&gt;医院里就是：病人需要医生亲自陪着做手术，医生被带走了。巡查员发现诊室空着，把钥匙交给另一位医生。手术结束，原来的医生带着病人回来，诊室已经有人了，就找间空诊室；一间都没有，就把病人送回大厅，自己去休息室。&lt;strong&gt;检查结果写在病人的病历里，不在医生脑子里&lt;/strong&gt;，所以谁接手都能接着看。&lt;/p&gt;&lt;/section&gt;
&lt;section&gt;&lt;h2&gt;八、网络 IO：netpoller 和值班医生&lt;a href=&quot;#八网络-ionetpoller-和值班医生&quot;&gt;&lt;span&gt;#&lt;/span&gt;&lt;/a&gt;&lt;/h2&gt;&lt;p&gt;网络 IO 不走上面那条路，而是交给&lt;strong&gt;网络轮询器&lt;/strong&gt;（netpoller），底层在 Linux 上是 epoll。&lt;/p&gt;&lt;p&gt;先说 epoll 本身：&lt;code&gt;epoll_ctl&lt;/code&gt; 把连接注册进内核里的 epoll 实例，网卡收到数据后，内核协议栈把数据放进 socket 缓冲区，同时通过注册时挂上的回调，把这个连接加入&lt;strong&gt;就绪列表&lt;/strong&gt;；&lt;code&gt;epoll_wait&lt;/code&gt; 只返回就绪的连接，不用遍历全部连接。&lt;/p&gt;&lt;p&gt;Go 里调用 &lt;code&gt;conn.Read&lt;/code&gt; 时：&lt;/p&gt;&lt;ol&gt;
&lt;li&gt;先非阻塞地读一次，有数据就直接返回；&lt;/li&gt;
&lt;li&gt;没数据，就把这个 G 挂起，挂在这个连接上（运行时用边缘触发的方式注册 epoll），M 去执行别的 G；&lt;/li&gt;
&lt;li&gt;数据到了，某个 M 在找活的第 5 步查 epoll 时发现它就绪，把挂起的 G 放回运行队列；&lt;/li&gt;
&lt;li&gt;这个 G 被调度到时，从 &lt;code&gt;Read&lt;/code&gt; 那一行继续，读到数据返回。&lt;/li&gt;
&lt;/ol&gt;&lt;p&gt;开发者看到的是一个阻塞的 &lt;code&gt;Read&lt;/code&gt;，实际上线程一刻也没有被阻塞。这就是 Go 能用同步的写法得到异步性能的原因。&lt;/p&gt;&lt;section&gt;&lt;h3&gt;为什么只有一个 M 阻塞在 epoll_wait&lt;a href=&quot;#为什么只有一个-m-阻塞在-epoll_wait&quot;&gt;&lt;span&gt;#&lt;/span&gt;&lt;/a&gt;&lt;/h3&gt;&lt;p&gt;每个 M 找活时都会查 epoll，但同一时刻只有一个 M 阻塞在 &lt;code&gt;epoll_wait&lt;/code&gt; 上，区别在于查法：&lt;/p&gt;&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;非阻塞地查&lt;/strong&gt;（超时为 0，看一眼就走）：任何 M 找活时都会做；&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;阻塞地等&lt;/strong&gt;（一直等到有事件或定时器到期）：同一时刻&lt;strong&gt;只允许一个 M&lt;/strong&gt;。&lt;/li&gt;
&lt;/ul&gt;&lt;p&gt;M 走完所有步骤都没找到活，先把 P 交回空闲列表，再用原子操作去抢一个全局标记（运行时里是 &lt;code&gt;sched.lastpoll&lt;/code&gt;），抢到的那个去阻塞等待 epoll，其他 M 看到已经有人在等，直接休眠。&lt;/p&gt;&lt;p&gt;原因有三：一个就够了，epoll 实例只有一个，多个线程守着也拿不到更多东西；避免&lt;strong&gt;惊群&lt;/strong&gt;，多个线程阻塞在同一个 epoll 上，一个事件可能把它们都唤醒，最后只有一个拿到，其他白白经历一次内核态的唤醒和切换；而且这个 M 已经交出了 P，阻塞着也不影响别人干活。&lt;/p&gt;&lt;p&gt;事件来了，这个 M 被唤醒，拿到就绪的 G，抢一个空闲的 P 来执行，活多的话再唤醒其他 M。&lt;/p&gt;&lt;p&gt;医院里就是：样本送去外面化验，病人在结果通知中心登记一次就回候诊区，医生去看别人。全院都闲下来时，医生们交还诊室钥匙去休息室，&lt;strong&gt;只留一位值班医生守着检验科的电话&lt;/strong&gt;。结果一到，他把病人叫回来，人多再去叫醒其他医生。全院医生都守着同一部电话没有意义，电话一响全被吵醒，最后只有一个人接。&lt;/p&gt;&lt;p&gt;顺带一提，&lt;strong&gt;文件读写不走 netpoller&lt;/strong&gt;。在 Linux 上，普通文件对 epoll 来说永远是”可读”的，起不到等待的作用，所以 Go 把它当成普通的阻塞系统调用，靠第七节的转交 P 来兜底。&lt;/p&gt;&lt;/section&gt;&lt;/section&gt;
&lt;section&gt;&lt;h2&gt;九、sysmon：院务巡查员&lt;a href=&quot;#九sysmon院务巡查员&quot;&gt;&lt;span&gt;#&lt;/span&gt;&lt;/a&gt;&lt;/h2&gt;&lt;p&gt;前面几次出现的 sysmon，是运行时在程序启动时自动创建的后台线程，不需要配置，也关不掉。它&lt;strong&gt;不需要 P&lt;/strong&gt;，所以不占执行 Go 代码的名额。繁忙时大约每 20 微秒检查一次，长时间空闲时逐渐放慢到每 10 毫秒一次。&lt;/p&gt;&lt;p&gt;它做的事基本都是”兜底”：&lt;/p&gt;&lt;ul&gt;
&lt;li&gt;抢占运行超过 10ms 的 G；&lt;/li&gt;
&lt;li&gt;拿走卡在系统调用里的 P，转交给别的 M；&lt;/li&gt;
&lt;li&gt;超过 10ms 没人查过 epoll，就主动非阻塞地查一次，防止就绪的 G 没人捡；&lt;/li&gt;
&lt;li&gt;超过 2 分钟没发生 GC，就强制触发一次。&lt;/li&gt;
&lt;/ul&gt;&lt;p&gt;正常的调度都是每个 M 自己找活，sysmon 负责处理那些”没人主动处理就会卡住”的情况。&lt;/p&gt;&lt;/section&gt;
&lt;section&gt;&lt;h2&gt;十、动手观察调度器&lt;a href=&quot;#十动手观察调度器&quot;&gt;&lt;span&gt;#&lt;/span&gt;&lt;/a&gt;&lt;/h2&gt;&lt;p&gt;&lt;code&gt;GODEBUG=schedtrace=毫秒数&lt;/code&gt; 会让运行时定期打印调度器的状态。起 100 个纯计算的 goroutine，固定 4 个 P：&lt;/p&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;&lt;figure&gt;&lt;figcaption&gt;&lt;/figcaption&gt;&lt;pre&gt;&lt;code&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;1&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;package&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;main&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;2&lt;/div&gt;&lt;/div&gt;&lt;div&gt;
&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;3&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;import&lt;/span&gt;&lt;span&gt; (&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;4&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;  &lt;/span&gt;&lt;span&gt;&quot;sync&quot;&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;5&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;  &lt;/span&gt;&lt;span&gt;&quot;time&quot;&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;6&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;)&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;7&lt;/div&gt;&lt;/div&gt;&lt;div&gt;
&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;8&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;func&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;work&lt;/span&gt;&lt;span&gt;() {&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;9&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;  &lt;/span&gt;&lt;span&gt;sum&lt;/span&gt;&lt;span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;:=&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;/span&gt;&lt;span&gt;0&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;10&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;  &lt;/span&gt;&lt;span&gt;for&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;i&lt;/span&gt;&lt;span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;:=&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;/span&gt;&lt;span&gt;0&lt;/span&gt;&lt;span&gt;; &lt;/span&gt;&lt;span&gt;i&lt;/span&gt;&lt;span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;&amp;lt;&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;/span&gt;&lt;span&gt;50_000_000&lt;/span&gt;&lt;span&gt;; &lt;/span&gt;&lt;span&gt;i&lt;/span&gt;&lt;span&gt;&lt;span&gt;++&lt;/span&gt;&lt;span&gt; {&lt;/span&gt;&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;11&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;    &lt;/span&gt;&lt;span&gt;sum&lt;/span&gt;&lt;span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;+=&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;/span&gt;&lt;span&gt;i&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;12&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;&lt;span&gt;  &lt;/span&gt;&lt;/span&gt;&lt;span&gt;}&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;13&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;  &lt;/span&gt;&lt;span&gt;_&lt;/span&gt;&lt;span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;=&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;/span&gt;&lt;span&gt;sum&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;14&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;}&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;15&lt;/div&gt;&lt;/div&gt;&lt;div&gt;
&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;16&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;func&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;main&lt;/span&gt;&lt;span&gt;() {&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;17&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;  &lt;/span&gt;&lt;span&gt;var&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;wg&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;sync&lt;/span&gt;&lt;span&gt;.&lt;/span&gt;&lt;span&gt;WaitGroup&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;18&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;  &lt;/span&gt;&lt;span&gt;for&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;i&lt;/span&gt;&lt;span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;:=&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;/span&gt;&lt;span&gt;0&lt;/span&gt;&lt;span&gt;; &lt;/span&gt;&lt;span&gt;i&lt;/span&gt;&lt;span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;&amp;lt;&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;/span&gt;&lt;span&gt;100&lt;/span&gt;&lt;span&gt;; &lt;/span&gt;&lt;span&gt;i&lt;/span&gt;&lt;span&gt;&lt;span&gt;++&lt;/span&gt;&lt;span&gt; {&lt;/span&gt;&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;19&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;    &lt;/span&gt;&lt;span&gt;wg&lt;/span&gt;&lt;span&gt;.&lt;/span&gt;&lt;span&gt;Add&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;1&lt;/span&gt;&lt;span&gt;)&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;20&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;    &lt;/span&gt;&lt;span&gt;go&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;func&lt;/span&gt;&lt;span&gt;() {&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;21&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;      &lt;/span&gt;&lt;span&gt;defer&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;wg&lt;/span&gt;&lt;span&gt;.&lt;/span&gt;&lt;span&gt;Done&lt;/span&gt;&lt;span&gt;()&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;22&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;      &lt;/span&gt;&lt;span&gt;work&lt;/span&gt;&lt;span&gt;()&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;23&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;&lt;span&gt;    &lt;/span&gt;&lt;/span&gt;&lt;span&gt;}()&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;24&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;&lt;span&gt;  &lt;/span&gt;&lt;/span&gt;&lt;span&gt;}&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;25&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;  &lt;/span&gt;&lt;span&gt;wg&lt;/span&gt;&lt;span&gt;.&lt;/span&gt;&lt;span&gt;Wait&lt;/span&gt;&lt;span&gt;()&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;26&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;  &lt;/span&gt;&lt;span&gt;time&lt;/span&gt;&lt;span&gt;.&lt;/span&gt;&lt;span&gt;Sleep&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;10&lt;/span&gt;&lt;span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;*&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;/span&gt;&lt;span&gt;time&lt;/span&gt;&lt;span&gt;.&lt;/span&gt;&lt;span&gt;Millisecond&lt;/span&gt;&lt;span&gt;)&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;27&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;}&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;/code&gt;&lt;/pre&gt;&lt;div&gt;&lt;div&gt;&lt;/div&gt;&lt;div&gt;&lt;/div&gt;&lt;/div&gt;&lt;/figure&gt;&lt;div&gt;&lt;/div&gt;&lt;/div&gt;&lt;span&gt;展开&lt;/span&gt;&lt;span&gt;收起&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;figure&gt;&lt;figcaption&gt;&lt;/figcaption&gt;&lt;pre&gt;&lt;code&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;1&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;$ GOMAXPROCS=4 GODEBUG=schedtrace=200 go run main.go&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;2&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;SCHED 0ms: gomaxprocs=4 idleprocs=1 threads=5 spinningthreads=1 needspinning=0 idlethreads=1 runqueue=0 [0 0 0 0]&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;3&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;SCHED 201ms: gomaxprocs=4 idleprocs=0 threads=5 spinningthreads=0 needspinning=1 idlethreads=0 runqueue=12 [11 11 10 12]&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;4&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;SCHED 406ms: gomaxprocs=4 idleprocs=0 threads=5 spinningthreads=0 needspinning=1 idlethreads=0 runqueue=3 [0 2 1 2]&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;/code&gt;&lt;/pre&gt;&lt;div&gt;&lt;div&gt;&lt;/div&gt;&lt;div&gt;&lt;/div&gt;&lt;/div&gt;&lt;/figure&gt;&lt;/div&gt;&lt;p&gt;各字段的意思：&lt;/p&gt;&lt;ul&gt;
&lt;li&gt;&lt;code&gt;gomaxprocs=4&lt;/code&gt;：4 个 P；&lt;/li&gt;
&lt;li&gt;&lt;code&gt;idleprocs&lt;/code&gt;：空闲的 P，跑起来之后是 0，四间诊室都在看病；&lt;/li&gt;
&lt;li&gt;&lt;code&gt;threads=5&lt;/code&gt;：一共 5 个线程，4 个干活的 M，再加 sysmon 等；&lt;/li&gt;
&lt;li&gt;&lt;code&gt;spinningthreads&lt;/code&gt;：正在”找活”的 M，刚启动时有 1 个；&lt;/li&gt;
&lt;li&gt;&lt;code&gt;runqueue=12&lt;/code&gt;：全局队列里有 12 个 G；&lt;/li&gt;
&lt;li&gt;&lt;code&gt;[11 11 10 12]&lt;/code&gt;：4 个 P 的本地队列各有多少个 G。&lt;/li&gt;
&lt;/ul&gt;&lt;p&gt;可以看到，100 个 G 并没有挤在一个 P 上：一部分在全局队列，其余分散在 4 个 P 的本地队列，随着时间推移被逐步消化。线程数一直是 5 个，不会因为 goroutine 多就创建更多线程。&lt;/p&gt;&lt;/section&gt;
&lt;section&gt;&lt;h2&gt;十一、和 Python、Node.js 对比&lt;a href=&quot;#十一和-pythonnodejs-对比&quot;&gt;&lt;span&gt;#&lt;/span&gt;&lt;/a&gt;&lt;/h2&gt;

&lt;table&gt;&lt;thead&gt;&lt;tr&gt;&lt;th&gt;&lt;/th&gt;&lt;th&gt;Python asyncio&lt;/th&gt;&lt;th&gt;Node.js&lt;/th&gt;&lt;th&gt;Go&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;一个&lt;/td&gt;&lt;td&gt;多个 M，大约等于 P 的数量&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;每个 M 各自找活，共享全局队列、互相偷&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;什么时候切换&lt;/td&gt;&lt;td&gt;代码写到 &lt;code&gt;await&lt;/code&gt;&lt;/td&gt;&lt;td&gt;一个回调执行完&lt;/td&gt;&lt;td&gt;等 channel、锁、网络、系统调用，或运行超过 10ms 被抢占&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;网络 IO&lt;/td&gt;&lt;td&gt;epoll，就绪后执行回调&lt;/td&gt;&lt;td&gt;epoll，就绪后执行回调&lt;/td&gt;&lt;td&gt;epoll，就绪后唤醒挂起的 G&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;代码写法&lt;/td&gt;&lt;td&gt;必须 &lt;code&gt;async/await&lt;/code&gt;&lt;/td&gt;&lt;td&gt;回调或 &lt;code&gt;async/await&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;不能，要配合多进程&lt;/td&gt;&lt;td&gt;不能，要配合多进程或 worker&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;div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;&lt;p&gt;内核态（三者共用）&lt;/p&gt;&lt;/span&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;&lt;p&gt;用户态&lt;/p&gt;&lt;/span&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;&lt;p&gt;Go&lt;/p&gt;&lt;/span&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;&lt;p&gt;Node.js&lt;/p&gt;&lt;/span&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;&lt;p&gt;Python asyncio&lt;/p&gt;&lt;/span&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;&lt;/span&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;&lt;/span&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;&lt;/span&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;&lt;/span&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;&lt;/span&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;&lt;/span&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;&lt;p&gt;系统调用&lt;/p&gt;&lt;/span&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;&lt;p&gt;系统调用&lt;/p&gt;&lt;/span&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;&lt;p&gt;系统调用&lt;/p&gt;&lt;/span&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;&lt;/span&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;&lt;/span&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;&lt;p&gt;很多协程&lt;br /&gt;&lt;br /&gt;await 处主动让出&lt;/p&gt;&lt;/span&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;&lt;p&gt;一个事件循环&lt;br /&gt;&lt;br /&gt;执行就绪的回调&lt;/p&gt;&lt;/span&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;&lt;p&gt;1 个线程&lt;br /&gt;&lt;br /&gt;多线程也受 GIL 限制&lt;/p&gt;&lt;/span&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;&lt;p&gt;回调和 Promise&lt;br /&gt;&lt;br /&gt;一个跑完才轮下一个&lt;/p&gt;&lt;/span&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;&lt;p&gt;libuv 事件循环&lt;br /&gt;&lt;br /&gt;按阶段执行回调&lt;/p&gt;&lt;/span&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;&lt;p&gt;1 个 JS 线程&lt;br /&gt;&lt;br /&gt;外加文件 IO 线程池&lt;/p&gt;&lt;/span&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;&lt;p&gt;很多 goroutine&lt;br /&gt;&lt;br /&gt;运行时自动挂起、抢占&lt;/p&gt;&lt;/span&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;&lt;p&gt;GMP 调度&lt;br /&gt;&lt;br /&gt;P 的数量 = 核数&lt;/p&gt;&lt;/span&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;&lt;p&gt;多个 M 线程&lt;br /&gt;&lt;br /&gt;同时执行 Go 代码&lt;/p&gt;&lt;/span&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;&lt;p&gt;epoll：告诉用户态哪些连接就绪了&lt;/p&gt;&lt;/span&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;&lt;p&gt;线程调度：决定哪个线程在哪个核上跑&lt;/p&gt;&lt;/span&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;&lt;p&gt;CPU 核 0 ～ 核 3&lt;/p&gt;&lt;/span&gt;&lt;/div&gt;
&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;&lt;p&gt;内核态（三者共用）&lt;/p&gt;&lt;/span&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;&lt;p&gt;用户态&lt;/p&gt;&lt;/span&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;&lt;p&gt;Go&lt;/p&gt;&lt;/span&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;&lt;p&gt;Node.js&lt;/p&gt;&lt;/span&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;&lt;p&gt;Python asyncio&lt;/p&gt;&lt;/span&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;&lt;/span&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;&lt;/span&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;&lt;/span&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;&lt;/span&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;&lt;/span&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;&lt;/span&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;&lt;p&gt;系统调用&lt;/p&gt;&lt;/span&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;&lt;p&gt;系统调用&lt;/p&gt;&lt;/span&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;&lt;p&gt;系统调用&lt;/p&gt;&lt;/span&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;&lt;/span&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;&lt;/span&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;&lt;p&gt;很多协程&lt;br /&gt;&lt;br /&gt;await 处主动让出&lt;/p&gt;&lt;/span&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;&lt;p&gt;一个事件循环&lt;br /&gt;&lt;br /&gt;执行就绪的回调&lt;/p&gt;&lt;/span&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;&lt;p&gt;1 个线程&lt;br /&gt;&lt;br /&gt;多线程也受 GIL 限制&lt;/p&gt;&lt;/span&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;&lt;p&gt;回调和 Promise&lt;br /&gt;&lt;br /&gt;一个跑完才轮下一个&lt;/p&gt;&lt;/span&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;&lt;p&gt;libuv 事件循环&lt;br /&gt;&lt;br /&gt;按阶段执行回调&lt;/p&gt;&lt;/span&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;&lt;p&gt;1 个 JS 线程&lt;br /&gt;&lt;br /&gt;外加文件 IO 线程池&lt;/p&gt;&lt;/span&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;&lt;p&gt;很多 goroutine&lt;br /&gt;&lt;br /&gt;运行时自动挂起、抢占&lt;/p&gt;&lt;/span&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;&lt;p&gt;GMP 调度&lt;br /&gt;&lt;br /&gt;P 的数量 = 核数&lt;/p&gt;&lt;/span&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;&lt;p&gt;多个 M 线程&lt;br /&gt;&lt;br /&gt;同时执行 Go 代码&lt;/p&gt;&lt;/span&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;&lt;p&gt;epoll：告诉用户态哪些连接就绪了&lt;/p&gt;&lt;/span&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;&lt;p&gt;线程调度：决定哪个线程在哪个核上跑&lt;/p&gt;&lt;/span&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;&lt;p&gt;CPU 核 0 ～ 核 3&lt;/p&gt;&lt;/span&gt;&lt;/div&gt;
&lt;/div&gt;&lt;/div&gt;&lt;/div&gt;&lt;p&gt;横着看三列、竖着看三层：&lt;/p&gt;&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;任务切换都在用户态&lt;/strong&gt;：协程、回调、goroutine 之间切换都不用进内核，区别只在”谁决定切”：Python 和 Node 靠代码写到 &lt;code&gt;await&lt;/code&gt; 或回调结束，Go 由运行时自动决定；&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;差别集中在线程这一层&lt;/strong&gt;：Python 和 Node 最终只有一个线程执行业务代码，内核只能把它放到一个核上；Go 有多个 M，内核可以把它们分到多个核上同时跑；&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;内核这一层三者完全一样&lt;/strong&gt;：都通过系统调用使用 epoll 等待网络事件，线程都交给内核调度到 CPU 上。&lt;/li&gt;
&lt;/ul&gt;&lt;p&gt;套回医院：Python asyncio 是&lt;strong&gt;只有一个诊室、一个医生&lt;/strong&gt;的诊所，医生自己当分诊台，病人只有自己说”我去检查了”才让出位置，医生要是被病人拉去陪检查，整个诊所就停了。Go 是&lt;strong&gt;多诊室的医院&lt;/strong&gt;：诊室数等于核数，医生可以比诊室多，有大厅和护士站统一调配，有巡查员兜底，医生闲了还能去别的诊室领病人。病人只管拿好自己的病历夹，换哪个医生、在哪个诊室看，都不影响结果。&lt;/p&gt;&lt;/section&gt;
&lt;section&gt;&lt;h2&gt;十二、速查清单&lt;a href=&quot;#十二速查清单&quot;&gt;&lt;span&gt;#&lt;/span&gt;&lt;/a&gt;&lt;/h2&gt;&lt;ul&gt;
&lt;li&gt;一个 Go 程序是&lt;strong&gt;单进程多线程&lt;/strong&gt;，G、M、P 都在这个进程里；&lt;/li&gt;
&lt;li&gt;P 的数量默认等于核数，是线程执行 Go 代码的通行证，不绑定具体的核；&lt;/li&gt;
&lt;li&gt;新建的 G 进当前 P 的 &lt;code&gt;runnext&lt;/code&gt;，本地队列最多 256 个，满了一半挪到全局队列；&lt;/li&gt;
&lt;li&gt;队列整体先进先出，只有最新创建的那个能通过 &lt;code&gt;runnext&lt;/code&gt; 插队；&lt;/li&gt;
&lt;li&gt;每个 M 自己找活：&lt;code&gt;runnext&lt;/code&gt; 和本地队列 → 全局队列 → 非阻塞查 netpoller → 随机偷别的 P 一半 → 交出 P 休眠；&lt;/li&gt;
&lt;li&gt;每调度 61 次先查一次全局队列，防止饿死；&lt;/li&gt;
&lt;li&gt;sysmon 发现 G 运行超过 10ms，Go 1.14 起通过 &lt;code&gt;SIGURG&lt;/code&gt; 信号异步抢占，被抢占的 G 进全局队列；&lt;/li&gt;
&lt;li&gt;系统调用阻塞时 P 会被转交给别的 M，返回后的 G 交给任意一个有 P 的 M 继续执行，结果在 G 的上下文里；&lt;/li&gt;
&lt;li&gt;网络 IO 挂到 netpoller，同一时刻只有一个 M 阻塞在 &lt;code&gt;epoll_wait&lt;/code&gt;，避免惊群；文件读写不走 netpoller；&lt;/li&gt;
&lt;li&gt;sysmon 是不需要 P 的后台线程，负责抢占、回收系统调用中的 P、兜底查 epoll、定期触发 GC；&lt;/li&gt;
&lt;li&gt;想观察调度器：&lt;code&gt;GODEBUG=schedtrace=1000&lt;/code&gt;；排查异步抢占相关的问题：&lt;code&gt;GODEBUG=asyncpreemptoff=1&lt;/code&gt;。&lt;/li&gt;
&lt;/ul&gt;&lt;p&gt;参考：&lt;/p&gt;&lt;ul&gt;
&lt;li&gt;&lt;a href=&quot;https://github.com/golang/go/blob/go1.24.7/src/runtime/proc.go&quot; target=&quot;_blank&quot;&gt;Go 运行时调度器源码 runtime/proc.go（Go 1.24.7）&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://github.com/golang/go/blob/go1.24.7/src/runtime/runtime2.go&quot; target=&quot;_blank&quot;&gt;Go 运行时数据结构 runtime/runtime2.go（Go 1.24.7）&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://github.com/golang/go/blob/go1.24.7/src/runtime/netpoll_epoll.go&quot; target=&quot;_blank&quot;&gt;Go 运行时 epoll 实现 runtime/netpoll_epoll.go（Go 1.24.7）&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;&lt;/section&gt;</content:encoded></item><item><title>让 Mac 和 iPhone 指定网站走 eSIM：sing-box 分流实录</title><link>https://tracerzzz.cn/posts/mac-hotspot-split-routing/</link><guid isPermaLink="true">https://tracerzzz.cn/posts/mac-hotspot-split-routing/</guid><description>家里 Wi-Fi 访问一部分境外网站不稳定，iPhone 上的境外 eSIM 访问正常但流量很少。这篇记录怎么让 Mac（通过手机热点）和 iPhone 自己都只把指定网站分给 eSIM，其他流量照常走 Wi-Fi。最后两台设备共用一份 sing-box 配置，用 iCloud 同步，规则全部来自社区规则集，基本不用手动维护。附完整配置、一路补过的漏洞和换设备时的快速清单。</description><pubDate>Tue, 27 Oct 2026 11:40:00 GMT</pubDate><content:encoded>&lt;blockquote&gt;&lt;p&gt;&lt;strong&gt;2026-10-31 更新&lt;/strong&gt;：Mac 也从 Clash Verge Rev 换成了 sing-box，和 iPhone 共用同一份配置，放在 iCloud 云盘里同步；手写的域名规则全部换成了社区规则集；记录了换过来之后国内网站全部打不开的排查过程。Clash 方案保留在第三节作为过程记录，完整配置以第五节为准。&lt;/p&gt;&lt;/blockquote&gt;
&lt;p&gt;我的 iPhone 是双卡：主号是国内卡，日常用；另外有一张境外 eSIM，访问境外网站很顺畅，但流量很少。家里的 Wi-Fi 访问 Claude、GitHub、Google 这类境外网站不稳定，Steam 商店也时好时坏。&lt;/p&gt;
&lt;p&gt;我想要的效果是，不管在 Mac 上还是在 iPhone 上：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;指定的境外网站&lt;/strong&gt;走 eSIM&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;其他所有流量&lt;/strong&gt;（国内网站、游戏下载、系统更新）照常走家里 Wi-Fi&lt;/li&gt;
&lt;li&gt;eSIM 流量能省则省，不能有流量在我不知道的时候偷偷走 eSIM&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;最后两台设备都用 sing-box 实现了，而且用的是&lt;strong&gt;同一份配置文件&lt;/strong&gt;。中间先后试过静态路由和 Clash Verge Rev，也补了好几轮漏洞，这篇把过程和原理讲清楚，最后附上换设备时的快速操作清单。&lt;/p&gt;
&lt;section&gt;&lt;h2&gt;一、先搞懂几个概念&lt;a href=&quot;#一先搞懂几个概念&quot;&gt;&lt;span&gt;#&lt;/span&gt;&lt;/a&gt;&lt;/h2&gt;&lt;section&gt;&lt;h3&gt;网卡名和 IP 不是一回事&lt;a href=&quot;#网卡名和-ip-不是一回事&quot;&gt;&lt;span&gt;#&lt;/span&gt;&lt;/a&gt;&lt;/h3&gt;&lt;p&gt;终端里运行 &lt;code&gt;ifconfig&lt;/code&gt;，会看到 &lt;code&gt;en0&lt;/code&gt;、&lt;code&gt;en6&lt;/code&gt; 这样的名字，下面跟着一串 IP 地址。&lt;/p&gt;&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;网卡名&lt;/strong&gt;是系统给硬件起的编号。&lt;code&gt;en0&lt;/code&gt; 一般就是这台 Mac 的 Wi-Fi 网卡，不管连哪个网络都叫 &lt;code&gt;en0&lt;/code&gt;。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;IP 地址&lt;/strong&gt;是当前连着的那个网络临时分给你的。连家里 Wi-Fi 是 &lt;code&gt;192.168.1.x&lt;/code&gt;，连 iPhone 热点就是 &lt;code&gt;172.20.10.x&lt;/code&gt;。&lt;/li&gt;
&lt;/ul&gt;&lt;p&gt;可以把网卡名理解成&lt;strong&gt;门牌号&lt;/strong&gt;，IP 是&lt;strong&gt;这次住进来的人&lt;/strong&gt;：门牌固定，住的人随网络变化。所以配置分流时不能认 IP。&lt;/p&gt;&lt;/section&gt;&lt;section&gt;&lt;h3&gt;Mac 要让两个网络同时在线&lt;a href=&quot;#mac-要让两个网络同时在线&quot;&gt;&lt;span&gt;#&lt;/span&gt;&lt;/a&gt;&lt;/h3&gt;&lt;p&gt;Mac 只有一块 Wi-Fi 网卡，同一时间只能连一个 Wi-Fi。如果 Wi-Fi 连的是手机热点，那所有流量都在走热点，没有东西可以「分」。&lt;/p&gt;&lt;p&gt;正确的连法是：&lt;/p&gt;&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Mac 的 Wi-Fi&lt;/strong&gt; 连家里路由器，做主网络&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;iPhone 用数据线&lt;/strong&gt;连 Mac，打开个人热点，Mac 上会多出一块叫「iPhone USB」的网卡&lt;/li&gt;
&lt;/ul&gt;&lt;p&gt;两块网卡各连各的，这才叫同时在线。两个网络都在线时，系统默认把流量交给&lt;strong&gt;服务顺序排在前面&lt;/strong&gt;的那个。在「系统设置 → 网络」里点列表下方的「⋯」→「设定服务顺序」，要保证 &lt;strong&gt;Wi-Fi 排在 iPhone USB 上面&lt;/strong&gt;。&lt;/p&gt;&lt;/section&gt;&lt;section&gt;&lt;h3&gt;iPhone 双卡同一时间只有一张卡能上网&lt;a href=&quot;#iphone-双卡同一时间只有一张卡能上网&quot;&gt;&lt;span&gt;#&lt;/span&gt;&lt;/a&gt;&lt;/h3&gt;&lt;p&gt;iPhone 的双卡是「双卡双待」：两张卡都能待机接电话，但&lt;strong&gt;同一时间只有一张卡连着数据网络&lt;/strong&gt;，就是「设置 → 蜂窝网络 → 蜂窝数据」里选中的那张。另一张卡的数据通道根本没有建立。&lt;/p&gt;&lt;p&gt;这带来两个结论：&lt;/p&gt;&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;个人热点走的是当前的数据卡。&lt;/strong&gt; 要让 Mac 通过热点用上 eSIM，数据卡就得是 eSIM。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;iPhone 做不到「按网站自动换卡」。&lt;/strong&gt; 切换数据卡要好几秒，而且会断开所有连接，App 也没有权限切换。iPhone 上能做的分流是 &lt;strong&gt;Wi-Fi 和蜂窝之间&lt;/strong&gt;的分流：在家时 iPhone 同时有家里 Wi-Fi 和蜂窝数据（eSIM）两个出口，按网站选其中一个。&lt;/li&gt;
&lt;/ul&gt;&lt;p&gt;另外，&lt;strong&gt;iPhone 的个人热点只共享蜂窝数据，从不共享它连着的 Wi-Fi&lt;/strong&gt;。热点共享出去的流量也不经过 iPhone 上的 sing-box，Mac 的分流由 Mac 自己的 sing-box 负责，两边互不干扰。&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;iPhone（连着家里 Wi-Fi）&lt;/td&gt;&lt;td&gt;家里 Wi-Fi&lt;/td&gt;&lt;td&gt;蜂窝 → eSIM&lt;/td&gt;&lt;td&gt;iPhone 上的 sing-box&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;Mac&lt;/td&gt;&lt;td&gt;家里 Wi-Fi&lt;/td&gt;&lt;td&gt;iPhone USB 热点 → eSIM&lt;/td&gt;&lt;td&gt;Mac 上的 sing-box&lt;/td&gt;&lt;/tr&gt;&lt;/tbody&gt;&lt;/table&gt;&lt;p&gt;前提只有一个：&lt;strong&gt;在家时 iPhone 的数据卡要设成 eSIM&lt;/strong&gt;。出门没有 Wi-Fi 时，把数据卡切回主号就行。&lt;/p&gt;&lt;/section&gt;&lt;/section&gt;
&lt;section&gt;&lt;h2&gt;二、Mac 上的第一次尝试：静态路由&lt;a href=&quot;#二mac-上的第一次尝试静态路由&quot;&gt;&lt;span&gt;#&lt;/span&gt;&lt;/a&gt;&lt;/h2&gt;&lt;p&gt;最直接的想法是改路由表：告诉系统「去这几个 IP 的流量，交给热点的网关」。&lt;/p&gt;&lt;div&gt;&lt;figure&gt;&lt;figcaption&gt;&lt;span&gt;&lt;/span&gt;&lt;span&gt;Terminal window&lt;/span&gt;&lt;/figcaption&gt;&lt;pre&gt;&lt;code&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;1&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;sudo&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;route&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;add&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;-net&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;160.79.104.0/23&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;172.20.10.1&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;/code&gt;&lt;/pre&gt;&lt;div&gt;&lt;div&gt;&lt;/div&gt;&lt;div&gt;&lt;/div&gt;&lt;/div&gt;&lt;/figure&gt;&lt;/div&gt;&lt;p&gt;&lt;code&gt;172.20.10.1&lt;/code&gt; 是 iPhone 热点的网关。我写了个脚本，定时解析域名、给解析出来的 IP 加路由，后来还做成了一个带开关的 SwiftUI 小应用。对 Claude 这种域名少、IP 段固定的服务确实好用，但扩展到更多网站时问题就来了：&lt;/p&gt;&lt;ol&gt;
&lt;li&gt;&lt;strong&gt;只能按 IP 分，不能按域名分。&lt;/strong&gt; 路由表只认 IP，每个域名都要先解析成 IP 再加路由，子域名也得一个个写，&lt;code&gt;*.google.com&lt;/code&gt; 这种通配写法用不了。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;DNS 解析结果不一定可用。&lt;/strong&gt; 有些境外域名在家里网络上解析，拿到的地址本身就连不上，把这种地址路由到热点也没用。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;CDN 的 IP 会共用和变化。&lt;/strong&gt; 同一个 CDN 节点可能同时服务很多网站，解析结果还经常变。&lt;/li&gt;
&lt;/ol&gt;&lt;p&gt;几十个网站还能凑合，常用的境外网站有几千个域名，这条路走不通。&lt;/p&gt;&lt;/section&gt;
&lt;section&gt;&lt;h2&gt;三、Mac 的第二个方案：Clash Verge Rev&lt;a href=&quot;#三mac-的第二个方案clash-verge-rev&quot;&gt;&lt;span&gt;#&lt;/span&gt;&lt;/a&gt;&lt;/h2&gt;&lt;p&gt;Clash Verge Rev 是一个开源的网络规则工具，内核叫 mihomo。很多人用它配合代理服务，但它也可以&lt;strong&gt;完全不用代理&lt;/strong&gt;，只做一件事：按规则决定每条连接从哪块网卡出去。这一节记录它的原理，现在的 sing-box 方案也是同样的思路。&lt;/p&gt;&lt;section&gt;&lt;h3&gt;数据是怎么走的&lt;a href=&quot;#数据是怎么走的&quot;&gt;&lt;span&gt;#&lt;/span&gt;&lt;/a&gt;&lt;/h3&gt;&lt;p&gt;打开「虚拟网卡模式」（TUN）后，系统里会多出一块虚拟网卡，所有流量都先进这块网卡，再由规则决定去向：&lt;/p&gt;&lt;div&gt;&lt;figure&gt;&lt;figcaption&gt;&lt;/figcaption&gt;&lt;pre&gt;&lt;code&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;1&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;应用（浏览器、Steam、Claude）&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;2&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;&lt;span&gt;        &lt;/span&gt;&lt;/span&gt;&lt;span&gt;│  所有 DNS 查询和连接&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;3&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;&lt;span&gt;        &lt;/span&gt;&lt;/span&gt;&lt;span&gt;▼&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;4&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;虚拟网卡 utun（mihomo 接管）&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;5&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;&lt;span&gt;        &lt;/span&gt;&lt;/span&gt;&lt;span&gt;│  按域名 / IP 匹配规则&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;6&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;&lt;span&gt;        &lt;/span&gt;&lt;/span&gt;&lt;span&gt;▼&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;7&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;&lt;span&gt;   &lt;/span&gt;&lt;/span&gt;&lt;span&gt;┌────┴─────────────┐&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;8&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;&lt;span&gt;   &lt;/span&gt;&lt;/span&gt;&lt;span&gt;▼                  ▼&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;9&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;Wi-Fi（DIRECT）     iPhone USB（热点 → eSIM）&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;10&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;国内网站、下载        Claude、GitHub、Google、Steam 商店&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;/code&gt;&lt;/pre&gt;&lt;div&gt;&lt;div&gt;&lt;/div&gt;&lt;div&gt;&lt;/div&gt;&lt;/div&gt;&lt;/figure&gt;&lt;/div&gt;&lt;p&gt;以打开 google.com 为例：&lt;/p&gt;&lt;ol&gt;
&lt;li&gt;&lt;strong&gt;浏览器问「google.com 的 IP 是多少」。&lt;/strong&gt; 这个 DNS 查询被虚拟网卡截住，mihomo 不去真查，而是立刻回一个假地址（比如 &lt;code&gt;198.18.0.5&lt;/code&gt;），同时记下「&lt;code&gt;198.18.0.5&lt;/code&gt; 就是 google.com」。这叫 &lt;strong&gt;fake-ip 模式&lt;/strong&gt;。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;浏览器去连 &lt;code&gt;198.18.0.5&lt;/code&gt;。&lt;/strong&gt; 这个连接也进了虚拟网卡。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;mihomo 查回域名，匹配规则。&lt;/strong&gt; 根据假 IP 查回 &lt;code&gt;google.com&lt;/code&gt;，命中「Google 走热点」的规则。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;mihomo 从热点网卡发起真正的连接。&lt;/strong&gt; 它先经热点用海外 DNS 查到真实地址，再连过去，然后在两头之间搬运数据。&lt;/li&gt;
&lt;/ol&gt;&lt;p&gt;这个设计解决了静态路由的三个问题：能&lt;strong&gt;按域名分流&lt;/strong&gt;，子域名自动覆盖；需要走热点的域名&lt;strong&gt;经热点用海外 DNS 解析&lt;/strong&gt;，结果可靠；还有社区维护的&lt;strong&gt;现成规则集&lt;/strong&gt;，几千个域名开箱即用。mihomo 自己发出去的连接不会又被吸回虚拟网卡，因为它发起连接时会直接&lt;strong&gt;绑定在指定的真实网卡上&lt;/strong&gt;。&lt;/p&gt;&lt;/section&gt;&lt;section&gt;&lt;h3&gt;用了几天后的问题&lt;a href=&quot;#用了几天后的问题&quot;&gt;&lt;span&gt;#&lt;/span&gt;&lt;/a&gt;&lt;/h3&gt;&lt;p&gt;Clash 方案能用，但有几处不顺手：&lt;/p&gt;&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;要写死热点网卡名。&lt;/strong&gt; 配置里的出口要写 &lt;code&gt;interface-name: en6&lt;/code&gt; 这样的网卡名，每台电脑、每部手机可能都不一样，我为此还写了一个自动识别网卡、改配置的脚本。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;和 iPhone 是两套配置。&lt;/strong&gt; iPhone 上只能用 sing-box，两边的配置格式完全不同，加一个网站要改两个地方。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;后台多一个常驻服务。&lt;/strong&gt; 虚拟网卡模式要装一个系统服务，加上界面程序，对这台 2016 年的老 MacBook Pro 来说不算轻。&lt;/li&gt;
&lt;/ul&gt;&lt;p&gt;后来发现 sing-box 在 Mac 上也能用同一份配置，而且不用写网卡名，就整体换了过去。&lt;/p&gt;&lt;/section&gt;&lt;/section&gt;
&lt;section&gt;&lt;h2&gt;四、最终方案：一份 sing-box 配置，两台设备共用&lt;a href=&quot;#四最终方案一份-sing-box-配置两台设备共用&quot;&gt;&lt;span&gt;#&lt;/span&gt;&lt;/a&gt;&lt;/h2&gt;&lt;section&gt;&lt;h3&gt;两个出口&lt;a href=&quot;#两个出口&quot;&gt;&lt;span&gt;#&lt;/span&gt;&lt;/a&gt;&lt;/h3&gt;&lt;p&gt;&lt;a href=&quot;https://sing-box.sagernet.org/&quot; target=&quot;_blank&quot;&gt;sing-box&lt;/a&gt; 从 1.11 版开始，可以给直连出口&lt;a href=&quot;https://sing-box.sagernet.org/configuration/shared/dial/&quot; target=&quot;_blank&quot;&gt;指定网络类型&lt;/a&gt;（&lt;code&gt;network_type&lt;/code&gt;，可选 &lt;code&gt;wifi&lt;/code&gt;、&lt;code&gt;cellular&lt;/code&gt;、&lt;code&gt;ethernet&lt;/code&gt;、&lt;code&gt;other&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;th&gt;用于&lt;/th&gt;&lt;/tr&gt;&lt;/thead&gt;&lt;tbody&gt;&lt;tr&gt;&lt;td&gt;&lt;code&gt;default&lt;/code&gt;&lt;/td&gt;&lt;td&gt;普通直连&lt;/td&gt;&lt;td&gt;系统默认网络，在家就是 Wi-Fi&lt;/td&gt;&lt;td&gt;国内网站和其他所有流量&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;&lt;code&gt;esim&lt;/code&gt;&lt;/td&gt;&lt;td&gt;&lt;code&gt;network_type: [&quot;cellular&quot;, &quot;ethernet&quot;]&lt;/code&gt;&lt;/td&gt;&lt;td&gt;蜂窝或有线网卡&lt;/td&gt;&lt;td&gt;境外网站列表、Claude、ChatGPT、GitHub、Google&lt;/td&gt;&lt;/tr&gt;&lt;/tbody&gt;&lt;/table&gt;&lt;/section&gt;&lt;section&gt;&lt;h3&gt;为什么一份配置能在两台设备上用&lt;a href=&quot;#为什么一份配置能在两台设备上用&quot;&gt;&lt;span&gt;#&lt;/span&gt;&lt;/a&gt;&lt;/h3&gt;&lt;p&gt;关键就在 &lt;code&gt;esim&lt;/code&gt; 出口的 &lt;code&gt;[&quot;cellular&quot;, &quot;ethernet&quot;]&lt;/code&gt;：&lt;/p&gt;&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;在 iPhone 上&lt;/strong&gt;，有蜂窝网络，&lt;code&gt;esim&lt;/code&gt; 走蜂窝，也就是 eSIM。iPhone 没有有线网卡，&lt;code&gt;ethernet&lt;/code&gt; 用不到。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;在 Mac 上&lt;/strong&gt;，没有蜂窝网络，&lt;code&gt;esim&lt;/code&gt; 就走有线网卡。iPhone 用数据线开热点时，macOS 把它当成一块有线网卡，于是 &lt;code&gt;esim&lt;/code&gt; 正好走 iPhone 热点。家里网络走 Wi-Fi，和它分开。&lt;/li&gt;
&lt;/ul&gt;&lt;p&gt;我在 Mac 上实际验证过：日志里能看到 &lt;code&gt;outbound/direct[esim]: dial en6&lt;/code&gt;，&lt;code&gt;en6&lt;/code&gt; 就是 iPhone USB 热点。这样配置里&lt;strong&gt;不用写网卡名&lt;/strong&gt;，换手机、换电脑都不用改，Clash 时代那个识别网卡的脚本也就用不上了。&lt;/p&gt;&lt;p&gt;这个写法有两个前提：Mac 平时不能同时插网线上网，否则 sing-box 分不清哪块有线网卡是热点；以及第一节说的，服务顺序里 Wi-Fi 要排在 iPhone USB 前面。如果哪台 Mac 把 iPhone 热点识别成了别的类型，把 &lt;code&gt;ethernet&lt;/code&gt; 换成 &lt;code&gt;other&lt;/code&gt; 试试。&lt;/p&gt;&lt;/section&gt;&lt;section&gt;&lt;h3&gt;为什么不用 Clash 的「智能回落」&lt;a href=&quot;#为什么不用-clash-的智能回落&quot;&gt;&lt;span&gt;#&lt;/span&gt;&lt;/a&gt;&lt;/h3&gt;&lt;p&gt;Clash 配置里有一个「热点优先」组：热点断了就自动回落到 Wi-Fi。sing-box 这份配置里没有。原因是这些境外网站在家里网络上本来就访问不稳定，回落过去也打不开，而为了探测热点通不通，还要定时消耗热点流量。所以干脆不做：热点断了，这些网站就打不开，插上线就恢复。&lt;/p&gt;&lt;/section&gt;&lt;section&gt;&lt;h3&gt;用 iCloud 同步&lt;a href=&quot;#用-icloud-同步&quot;&gt;&lt;span&gt;#&lt;/span&gt;&lt;/a&gt;&lt;/h3&gt;&lt;p&gt;sing-box 的苹果客户端新建配置时可以选「iCloud」类型，读取 iCloud 云盘里 sing-box 文件夹中的 JSON 文件。我把配置放在那里，Mac 和 iPhone 读的是同一个文件，改一次两边都生效，具体见第八节。&lt;/p&gt;&lt;/section&gt;&lt;/section&gt;
&lt;section&gt;&lt;h2&gt;五、完整配置&lt;a href=&quot;#五完整配置&quot;&gt;&lt;span&gt;#&lt;/span&gt;&lt;/a&gt;&lt;/h2&gt;&lt;p&gt;保存为 &lt;code&gt;esim-split.json&lt;/code&gt;，放进「iCloud 云盘 → sing-box」文件夹。sing-box 从 1.14 起支持在配置文件里写 &lt;code&gt;//&lt;/code&gt; 注释，所以说明直接写在了文件里，复制整份就能用：&lt;/p&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;&lt;figure&gt;&lt;figcaption&gt;&lt;/figcaption&gt;&lt;pre&gt;&lt;code&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;1&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;// ============================================================&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;2&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;//  esim-split.json：Mac 和 iPhone 共用的 sing-box 分流配置&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;3&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;//  放在「iCloud 云盘 → sing-box」里，两台设备都选 iCloud 类型的配置读取它&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;4&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;//&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;5&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;//  效果：&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;6&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;//    - Claude、ChatGPT、GitHub、Google、常用境外网站 → 走 eSIM&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;7&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;//        iPhone 上是蜂窝数据（数据卡设为 eSIM）&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;8&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;//        Mac 上是 iPhone 用数据线开的热点&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;9&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;//    - 国内网站和其他所有流量 → 走家里 Wi-Fi&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;10&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;//&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;11&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;//  改完这个文件后，要在 sing-box 里把开关关掉再打开才会生效&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;12&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;//  需要 sing-box 1.14 及以上（支持 // 注释和 http_clients 写法）&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;13&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;// ============================================================&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;14&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;{&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;15&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;  &lt;/span&gt;&lt;span&gt;// ---------- 日志 ----------&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;16&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;  &lt;/span&gt;&lt;span&gt;// warn：只记录警告和错误。排查问题时可以临时改成 info，看每条连接的去向&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;17&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;  &lt;/span&gt;&lt;span&gt;&quot;log&quot;&lt;/span&gt;&lt;span&gt;: {&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;18&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;    &lt;/span&gt;&lt;span&gt;&quot;level&quot;&lt;/span&gt;&lt;span&gt;: &lt;/span&gt;&lt;span&gt;&quot;warn&quot;&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;19&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;&lt;span&gt;  &lt;/span&gt;&lt;/span&gt;&lt;span&gt;},&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;20&lt;/div&gt;&lt;/div&gt;&lt;div&gt;
&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;21&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;  &lt;/span&gt;&lt;span&gt;// ---------- DNS：域名解析 ----------&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;22&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;  &lt;/span&gt;&lt;span&gt;&quot;dns&quot;&lt;/span&gt;&lt;span&gt;: {&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;23&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;    &lt;/span&gt;&lt;span&gt;&quot;servers&quot;&lt;/span&gt;&lt;span&gt;: [&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;24&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;&lt;span&gt;      &lt;/span&gt;&lt;/span&gt;&lt;span&gt;{&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;25&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;        &lt;/span&gt;&lt;span&gt;// 境外 DNS（Cloudflare 的加密 DNS），查询本身也经 esim 出口发出&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;26&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;        &lt;/span&gt;&lt;span&gt;// 这样拿到的是境外网络视角下真实可用的地址&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;27&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;        &lt;/span&gt;&lt;span&gt;&quot;type&quot;&lt;/span&gt;&lt;span&gt;: &lt;/span&gt;&lt;span&gt;&quot;https&quot;&lt;/span&gt;&lt;span&gt;,&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;28&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;        &lt;/span&gt;&lt;span&gt;&quot;tag&quot;&lt;/span&gt;&lt;span&gt;: &lt;/span&gt;&lt;span&gt;&quot;dns-esim&quot;&lt;/span&gt;&lt;span&gt;,&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;29&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;        &lt;/span&gt;&lt;span&gt;&quot;server&quot;&lt;/span&gt;&lt;span&gt;: &lt;/span&gt;&lt;span&gt;&quot;1.1.1.1&quot;&lt;/span&gt;&lt;span&gt;,&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;30&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;        &lt;/span&gt;&lt;span&gt;&quot;detour&quot;&lt;/span&gt;&lt;span&gt;: &lt;/span&gt;&lt;span&gt;&quot;esim&quot;&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;31&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;&lt;span&gt;      &lt;/span&gt;&lt;/span&gt;&lt;span&gt;},&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;32&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;&lt;span&gt;      &lt;/span&gt;&lt;/span&gt;&lt;span&gt;{&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;33&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;        &lt;/span&gt;&lt;span&gt;// 国内 DNS（阿里的加密 DNS），走默认网络，解析国内网站又快又准&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;34&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;        &lt;/span&gt;&lt;span&gt;&quot;type&quot;&lt;/span&gt;&lt;span&gt;: &lt;/span&gt;&lt;span&gt;&quot;https&quot;&lt;/span&gt;&lt;span&gt;,&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;35&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;        &lt;/span&gt;&lt;span&gt;&quot;tag&quot;&lt;/span&gt;&lt;span&gt;: &lt;/span&gt;&lt;span&gt;&quot;dns-cn&quot;&lt;/span&gt;&lt;span&gt;,&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;36&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;        &lt;/span&gt;&lt;span&gt;&quot;server&quot;&lt;/span&gt;&lt;span&gt;: &lt;/span&gt;&lt;span&gt;&quot;223.5.5.5&quot;&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;37&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;&lt;span&gt;      &lt;/span&gt;&lt;/span&gt;&lt;span&gt;}&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;38&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;&lt;span&gt;    &lt;/span&gt;&lt;/span&gt;&lt;span&gt;],&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;39&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;    &lt;/span&gt;&lt;span&gt;&quot;rules&quot;&lt;/span&gt;&lt;span&gt;: [&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;40&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;&lt;span&gt;      &lt;/span&gt;&lt;/span&gt;&lt;span&gt;{&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;41&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;        &lt;/span&gt;&lt;span&gt;// 要走 eSIM 的域名只返回 IPv4 地址：境外 eSIM 漫游时只有 IPv4，&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;42&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;        &lt;/span&gt;&lt;span&gt;// 拿到 IPv6 地址会连不上（Claude 桌面版的长连接就会因此断开）&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;43&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;        &lt;/span&gt;&lt;span&gt;// 国内网站不受影响，照常可以用 IPv6&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;44&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;        &lt;/span&gt;&lt;span&gt;&quot;query_type&quot;&lt;/span&gt;&lt;span&gt;: [&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;45&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;          &lt;/span&gt;&lt;span&gt;&quot;AAAA&quot;&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;46&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;&lt;span&gt;        &lt;/span&gt;&lt;/span&gt;&lt;span&gt;],&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;47&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;        &lt;/span&gt;&lt;span&gt;&quot;rule_set&quot;&lt;/span&gt;&lt;span&gt;: [&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;48&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;          &lt;/span&gt;&lt;span&gt;&quot;anthropic&quot;&lt;/span&gt;&lt;span&gt;,&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;49&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;          &lt;/span&gt;&lt;span&gt;&quot;openai&quot;&lt;/span&gt;&lt;span&gt;,&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;50&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;          &lt;/span&gt;&lt;span&gt;&quot;overseas-list&quot;&lt;/span&gt;&lt;span&gt;,&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;51&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;          &lt;/span&gt;&lt;span&gt;&quot;github&quot;&lt;/span&gt;&lt;span&gt;,&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;52&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;          &lt;/span&gt;&lt;span&gt;&quot;google&quot;&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;53&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;&lt;span&gt;        &lt;/span&gt;&lt;/span&gt;&lt;span&gt;],&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;54&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;        &lt;/span&gt;&lt;span&gt;&quot;action&quot;&lt;/span&gt;&lt;span&gt;: &lt;/span&gt;&lt;span&gt;&quot;predefined&quot;&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;55&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;&lt;span&gt;      &lt;/span&gt;&lt;/span&gt;&lt;span&gt;},&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;56&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;&lt;span&gt;      &lt;/span&gt;&lt;/span&gt;&lt;span&gt;{&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;57&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;        &lt;/span&gt;&lt;span&gt;// 要走 eSIM 的域名，用境外 DNS 解析&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;58&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;        &lt;/span&gt;&lt;span&gt;&quot;rule_set&quot;&lt;/span&gt;&lt;span&gt;: [&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;59&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;          &lt;/span&gt;&lt;span&gt;&quot;anthropic&quot;&lt;/span&gt;&lt;span&gt;,&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;60&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;          &lt;/span&gt;&lt;span&gt;&quot;openai&quot;&lt;/span&gt;&lt;span&gt;,&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;61&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;          &lt;/span&gt;&lt;span&gt;&quot;overseas-list&quot;&lt;/span&gt;&lt;span&gt;,&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;62&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;          &lt;/span&gt;&lt;span&gt;&quot;github&quot;&lt;/span&gt;&lt;span&gt;,&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;63&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;          &lt;/span&gt;&lt;span&gt;&quot;google&quot;&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;64&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;&lt;span&gt;        &lt;/span&gt;&lt;/span&gt;&lt;span&gt;],&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;65&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;        &lt;/span&gt;&lt;span&gt;&quot;action&quot;&lt;/span&gt;&lt;span&gt;: &lt;/span&gt;&lt;span&gt;&quot;route&quot;&lt;/span&gt;&lt;span&gt;,&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;66&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;        &lt;/span&gt;&lt;span&gt;&quot;server&quot;&lt;/span&gt;&lt;span&gt;: &lt;/span&gt;&lt;span&gt;&quot;dns-esim&quot;&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;67&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;&lt;span&gt;      &lt;/span&gt;&lt;/span&gt;&lt;span&gt;}&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;68&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;&lt;span&gt;    &lt;/span&gt;&lt;/span&gt;&lt;span&gt;],&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;69&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;    &lt;/span&gt;&lt;span&gt;// 其他所有域名都用国内 DNS&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;70&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;    &lt;/span&gt;&lt;span&gt;&quot;final&quot;&lt;/span&gt;&lt;span&gt;: &lt;/span&gt;&lt;span&gt;&quot;dns-cn&quot;&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;71&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;&lt;span&gt;  &lt;/span&gt;&lt;/span&gt;&lt;span&gt;},&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;72&lt;/div&gt;&lt;/div&gt;&lt;div&gt;
&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;73&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;  &lt;/span&gt;&lt;span&gt;// ---------- 下载规则集用的 HTTP 客户端 ----------&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;74&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;  &lt;/span&gt;&lt;span&gt;// 规则集托管在 GitHub 上，家里网络访问不稳定，所以经 esim 出口下载&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;75&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;  &lt;/span&gt;&lt;span&gt;&quot;http_clients&quot;&lt;/span&gt;&lt;span&gt;: [&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;76&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;&lt;span&gt;    &lt;/span&gt;&lt;/span&gt;&lt;span&gt;{&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;77&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;      &lt;/span&gt;&lt;span&gt;&quot;tag&quot;&lt;/span&gt;&lt;span&gt;: &lt;/span&gt;&lt;span&gt;&quot;esim-http&quot;&lt;/span&gt;&lt;span&gt;,&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;78&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;      &lt;/span&gt;&lt;span&gt;&quot;detour&quot;&lt;/span&gt;&lt;span&gt;: &lt;/span&gt;&lt;span&gt;&quot;esim&quot;&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;79&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;&lt;span&gt;    &lt;/span&gt;&lt;/span&gt;&lt;span&gt;}&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;80&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;&lt;span&gt;  &lt;/span&gt;&lt;/span&gt;&lt;span&gt;],&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;81&lt;/div&gt;&lt;/div&gt;&lt;div&gt;
&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;82&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;  &lt;/span&gt;&lt;span&gt;// ---------- 入口：虚拟网卡 ----------&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;83&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;  &lt;/span&gt;&lt;span&gt;// 系统所有流量都先进这块虚拟网卡，再由下面的规则决定从哪个出口出去&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;84&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;  &lt;/span&gt;&lt;span&gt;&quot;inbounds&quot;&lt;/span&gt;&lt;span&gt;: [&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;85&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;&lt;span&gt;    &lt;/span&gt;&lt;/span&gt;&lt;span&gt;{&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;86&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;      &lt;/span&gt;&lt;span&gt;&quot;type&quot;&lt;/span&gt;&lt;span&gt;: &lt;/span&gt;&lt;span&gt;&quot;tun&quot;&lt;/span&gt;&lt;span&gt;,&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;87&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;      &lt;/span&gt;&lt;span&gt;&quot;tag&quot;&lt;/span&gt;&lt;span&gt;: &lt;/span&gt;&lt;span&gt;&quot;tun-in&quot;&lt;/span&gt;&lt;span&gt;,&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;88&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;      &lt;/span&gt;&lt;span&gt;// 虚拟网卡自己的地址，用的是不会和真实网络冲突的保留网段&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;89&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;      &lt;/span&gt;&lt;span&gt;&quot;address&quot;&lt;/span&gt;&lt;span&gt;: [&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;90&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;        &lt;/span&gt;&lt;span&gt;&quot;172.19.0.1/30&quot;&lt;/span&gt;&lt;span&gt;,&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;91&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;        &lt;/span&gt;&lt;span&gt;&quot;fdfe:dcba:9876::1/126&quot;&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;92&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;&lt;span&gt;      &lt;/span&gt;&lt;/span&gt;&lt;span&gt;],&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;93&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;      &lt;/span&gt;&lt;span&gt;// 自动把系统默认路由指向虚拟网卡&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;94&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;      &lt;/span&gt;&lt;span&gt;&quot;auto_route&quot;&lt;/span&gt;&lt;span&gt;: &lt;/span&gt;&lt;span&gt;true&lt;/span&gt;&lt;span&gt;,&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;95&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;      &lt;/span&gt;&lt;span&gt;// 严格路由：防止流量绕过虚拟网卡直接出去&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;96&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;      &lt;/span&gt;&lt;span&gt;&quot;strict_route&quot;&lt;/span&gt;&lt;span&gt;: &lt;/span&gt;&lt;span&gt;true&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;97&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;&lt;span&gt;    &lt;/span&gt;&lt;/span&gt;&lt;span&gt;}&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;98&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;&lt;span&gt;  &lt;/span&gt;&lt;/span&gt;&lt;span&gt;],&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;99&lt;/div&gt;&lt;/div&gt;&lt;div&gt;
&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;100&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;  &lt;/span&gt;&lt;span&gt;// ---------- 出口 ----------&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;101&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;  &lt;/span&gt;&lt;span&gt;&quot;outbounds&quot;&lt;/span&gt;&lt;span&gt;: [&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;102&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;&lt;span&gt;    &lt;/span&gt;&lt;/span&gt;&lt;span&gt;{&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;103&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;      &lt;/span&gt;&lt;span&gt;// 普通直连，跟随系统默认网络：在家就是 Wi-Fi&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;104&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;      &lt;/span&gt;&lt;span&gt;&quot;type&quot;&lt;/span&gt;&lt;span&gt;: &lt;/span&gt;&lt;span&gt;&quot;direct&quot;&lt;/span&gt;&lt;span&gt;,&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;105&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;      &lt;/span&gt;&lt;span&gt;&quot;tag&quot;&lt;/span&gt;&lt;span&gt;: &lt;/span&gt;&lt;span&gt;&quot;default&quot;&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;106&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;&lt;span&gt;    &lt;/span&gt;&lt;/span&gt;&lt;span&gt;},&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;107&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;&lt;span&gt;    &lt;/span&gt;&lt;/span&gt;&lt;span&gt;{&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;108&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;      &lt;/span&gt;&lt;span&gt;// 直连，但只走「蜂窝」或「有线」网络&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;109&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;      &lt;/span&gt;&lt;span&gt;//   iPhone：有蜂窝，走蜂窝数据（eSIM）；iPhone 没有有线网卡，ethernet 用不到&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;110&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;      &lt;/span&gt;&lt;span&gt;//   Mac：没有蜂窝，走有线网卡；iPhone 用数据线开的热点在 Mac 上就是一块有线网卡&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;111&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;      &lt;/span&gt;&lt;span&gt;// 所以同一份配置在两台设备上都正确，也不用写网卡名&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;112&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;      &lt;/span&gt;&lt;span&gt;// default 策略：只用这两类网络，它们不可用时不会偷偷改走 Wi-Fi&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;113&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;      &lt;/span&gt;&lt;span&gt;// 前提：Mac 不能同时插网线上网；如果某台 Mac 把热点识别成别的类型，把 ethernet 换成 other&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;114&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;      &lt;/span&gt;&lt;span&gt;&quot;type&quot;&lt;/span&gt;&lt;span&gt;: &lt;/span&gt;&lt;span&gt;&quot;direct&quot;&lt;/span&gt;&lt;span&gt;,&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;115&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;      &lt;/span&gt;&lt;span&gt;&quot;tag&quot;&lt;/span&gt;&lt;span&gt;: &lt;/span&gt;&lt;span&gt;&quot;esim&quot;&lt;/span&gt;&lt;span&gt;,&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;116&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;      &lt;/span&gt;&lt;span&gt;&quot;network_strategy&quot;&lt;/span&gt;&lt;span&gt;: &lt;/span&gt;&lt;span&gt;&quot;default&quot;&lt;/span&gt;&lt;span&gt;,&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;117&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;      &lt;/span&gt;&lt;span&gt;&quot;network_type&quot;&lt;/span&gt;&lt;span&gt;: [&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;118&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;        &lt;/span&gt;&lt;span&gt;&quot;cellular&quot;&lt;/span&gt;&lt;span&gt;,&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;119&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;        &lt;/span&gt;&lt;span&gt;&quot;ethernet&quot;&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;120&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;&lt;span&gt;      &lt;/span&gt;&lt;/span&gt;&lt;span&gt;]&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;121&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;&lt;span&gt;    &lt;/span&gt;&lt;/span&gt;&lt;span&gt;}&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;122&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;&lt;span&gt;  &lt;/span&gt;&lt;/span&gt;&lt;span&gt;],&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;123&lt;/div&gt;&lt;/div&gt;&lt;div&gt;
&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;124&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;  &lt;/span&gt;&lt;span&gt;// ---------- 分流 ----------&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;125&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;  &lt;/span&gt;&lt;span&gt;&quot;route&quot;&lt;/span&gt;&lt;span&gt;: {&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;126&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;    &lt;/span&gt;&lt;span&gt;// 自动找到真实网卡，sing-box 自己发出的连接直接绑定到真实网卡，不会又绕回虚拟网卡&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;127&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;    &lt;/span&gt;&lt;span&gt;&quot;auto_detect_interface&quot;&lt;/span&gt;&lt;span&gt;: &lt;/span&gt;&lt;span&gt;true&lt;/span&gt;&lt;span&gt;,&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;128&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;    &lt;/span&gt;&lt;span&gt;// sing-box 自己需要解析域名时（比如连接出口），默认用国内 DNS&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;129&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;    &lt;/span&gt;&lt;span&gt;&quot;default_domain_resolver&quot;&lt;/span&gt;&lt;span&gt;: &lt;/span&gt;&lt;span&gt;&quot;dns-cn&quot;&lt;/span&gt;&lt;span&gt;,&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;130&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;    &lt;/span&gt;&lt;span&gt;// 下载规则集时默认用上面的 esim-http&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;131&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;    &lt;/span&gt;&lt;span&gt;&quot;default_http_client&quot;&lt;/span&gt;&lt;span&gt;: &lt;/span&gt;&lt;span&gt;&quot;esim-http&quot;&lt;/span&gt;&lt;span&gt;,&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;132&lt;/div&gt;&lt;/div&gt;&lt;div&gt;
&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;133&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;    &lt;/span&gt;&lt;span&gt;// ---------- 规则集：全部来自社区，每 7 天自动更新 ----------&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;134&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;    &lt;/span&gt;&lt;span&gt;// 来源：https://github.com/MetaCubeX/meta-rules-dat 的 sing 分支&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;135&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;    &lt;/span&gt;&lt;span&gt;// 源头列表：https://github.com/v2fly/domain-list-community 的 data 目录&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;136&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;    &lt;/span&gt;&lt;span&gt;// geosite = 按域名分组，geoip = 按 IP 段分组&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;137&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;    &lt;/span&gt;&lt;span&gt;&quot;rule_set&quot;&lt;/span&gt;&lt;span&gt;: [&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;138&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;&lt;span&gt;      &lt;/span&gt;&lt;/span&gt;&lt;span&gt;{&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;139&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;        &lt;/span&gt;&lt;span&gt;// Claude / Anthropic 的全部域名，包括 Artifact 用的 claudeusercontent.com&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;140&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;        &lt;/span&gt;&lt;span&gt;&quot;type&quot;&lt;/span&gt;&lt;span&gt;: &lt;/span&gt;&lt;span&gt;&quot;remote&quot;&lt;/span&gt;&lt;span&gt;,&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;141&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;        &lt;/span&gt;&lt;span&gt;&quot;tag&quot;&lt;/span&gt;&lt;span&gt;: &lt;/span&gt;&lt;span&gt;&quot;anthropic&quot;&lt;/span&gt;&lt;span&gt;,&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;142&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;        &lt;/span&gt;&lt;span&gt;&quot;format&quot;&lt;/span&gt;&lt;span&gt;: &lt;/span&gt;&lt;span&gt;&quot;binary&quot;&lt;/span&gt;&lt;span&gt;,&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;143&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;        &lt;/span&gt;&lt;span&gt;&quot;url&quot;&lt;/span&gt;&lt;span&gt;: &lt;/span&gt;&lt;span&gt;&quot;https://raw.githubusercontent.com/MetaCubeX/meta-rules-dat/sing/geo/geosite/anthropic.srs&quot;&lt;/span&gt;&lt;span&gt;,&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;144&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;        &lt;/span&gt;&lt;span&gt;&quot;update_interval&quot;&lt;/span&gt;&lt;span&gt;: &lt;/span&gt;&lt;span&gt;&quot;7d&quot;&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;145&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;&lt;span&gt;      &lt;/span&gt;&lt;/span&gt;&lt;span&gt;},&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;146&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;&lt;span&gt;      &lt;/span&gt;&lt;/span&gt;&lt;span&gt;{&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;147&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;        &lt;/span&gt;&lt;span&gt;// ChatGPT / OpenAI 的全部域名，包括静态资源域名&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;148&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;        &lt;/span&gt;&lt;span&gt;&quot;type&quot;&lt;/span&gt;&lt;span&gt;: &lt;/span&gt;&lt;span&gt;&quot;remote&quot;&lt;/span&gt;&lt;span&gt;,&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;149&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;        &lt;/span&gt;&lt;span&gt;&quot;tag&quot;&lt;/span&gt;&lt;span&gt;: &lt;/span&gt;&lt;span&gt;&quot;openai&quot;&lt;/span&gt;&lt;span&gt;,&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;150&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;        &lt;/span&gt;&lt;span&gt;&quot;format&quot;&lt;/span&gt;&lt;span&gt;: &lt;/span&gt;&lt;span&gt;&quot;binary&quot;&lt;/span&gt;&lt;span&gt;,&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;151&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;        &lt;/span&gt;&lt;span&gt;&quot;url&quot;&lt;/span&gt;&lt;span&gt;: &lt;/span&gt;&lt;span&gt;&quot;https://raw.githubusercontent.com/MetaCubeX/meta-rules-dat/sing/geo/geosite/openai.srs&quot;&lt;/span&gt;&lt;span&gt;,&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;152&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;        &lt;/span&gt;&lt;span&gt;&quot;update_interval&quot;&lt;/span&gt;&lt;span&gt;: &lt;/span&gt;&lt;span&gt;&quot;7d&quot;&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;153&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;&lt;span&gt;      &lt;/span&gt;&lt;/span&gt;&lt;span&gt;},&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;154&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;&lt;span&gt;      &lt;/span&gt;&lt;/span&gt;&lt;span&gt;{&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;155&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;        &lt;/span&gt;&lt;span&gt;// 常用境外网站列表（上游文件名沿用项目原名）&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;156&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;        &lt;/span&gt;&lt;span&gt;// Steam 商店和社区、Telegram、X、YouTube、Grok 等都在里面&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;157&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;        &lt;/span&gt;&lt;span&gt;&quot;type&quot;&lt;/span&gt;&lt;span&gt;: &lt;/span&gt;&lt;span&gt;&quot;remote&quot;&lt;/span&gt;&lt;span&gt;,&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;158&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;        &lt;/span&gt;&lt;span&gt;&quot;tag&quot;&lt;/span&gt;&lt;span&gt;: &lt;/span&gt;&lt;span&gt;&quot;overseas-list&quot;&lt;/span&gt;&lt;span&gt;,&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;159&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;        &lt;/span&gt;&lt;span&gt;&quot;format&quot;&lt;/span&gt;&lt;span&gt;: &lt;/span&gt;&lt;span&gt;&quot;binary&quot;&lt;/span&gt;&lt;span&gt;,&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;160&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;        &lt;/span&gt;&lt;span&gt;&quot;url&quot;&lt;/span&gt;&lt;span&gt;: &lt;/span&gt;&lt;span&gt;&quot;https://raw.githubusercontent.com/MetaCubeX/meta-rules-dat/sing/geo/geosite/gfw.srs&quot;&lt;/span&gt;&lt;span&gt;,&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;161&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;        &lt;/span&gt;&lt;span&gt;&quot;update_interval&quot;&lt;/span&gt;&lt;span&gt;: &lt;/span&gt;&lt;span&gt;&quot;7d&quot;&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;162&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;&lt;span&gt;      &lt;/span&gt;&lt;/span&gt;&lt;span&gt;},&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;163&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;&lt;span&gt;      &lt;/span&gt;&lt;/span&gt;&lt;span&gt;{&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;164&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;        &lt;/span&gt;&lt;span&gt;// GitHub 全部域名&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;165&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;        &lt;/span&gt;&lt;span&gt;&quot;type&quot;&lt;/span&gt;&lt;span&gt;: &lt;/span&gt;&lt;span&gt;&quot;remote&quot;&lt;/span&gt;&lt;span&gt;,&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;166&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;        &lt;/span&gt;&lt;span&gt;&quot;tag&quot;&lt;/span&gt;&lt;span&gt;: &lt;/span&gt;&lt;span&gt;&quot;github&quot;&lt;/span&gt;&lt;span&gt;,&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;167&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;        &lt;/span&gt;&lt;span&gt;&quot;format&quot;&lt;/span&gt;&lt;span&gt;: &lt;/span&gt;&lt;span&gt;&quot;binary&quot;&lt;/span&gt;&lt;span&gt;,&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;168&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;        &lt;/span&gt;&lt;span&gt;&quot;url&quot;&lt;/span&gt;&lt;span&gt;: &lt;/span&gt;&lt;span&gt;&quot;https://raw.githubusercontent.com/MetaCubeX/meta-rules-dat/sing/geo/geosite/github.srs&quot;&lt;/span&gt;&lt;span&gt;,&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;169&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;        &lt;/span&gt;&lt;span&gt;&quot;update_interval&quot;&lt;/span&gt;&lt;span&gt;: &lt;/span&gt;&lt;span&gt;&quot;7d&quot;&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;170&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;&lt;span&gt;      &lt;/span&gt;&lt;/span&gt;&lt;span&gt;},&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;171&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;&lt;span&gt;      &lt;/span&gt;&lt;/span&gt;&lt;span&gt;{&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;172&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;        &lt;/span&gt;&lt;span&gt;// Google 全部域名，包括 Gemini&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;173&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;        &lt;/span&gt;&lt;span&gt;&quot;type&quot;&lt;/span&gt;&lt;span&gt;: &lt;/span&gt;&lt;span&gt;&quot;remote&quot;&lt;/span&gt;&lt;span&gt;,&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;174&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;        &lt;/span&gt;&lt;span&gt;&quot;tag&quot;&lt;/span&gt;&lt;span&gt;: &lt;/span&gt;&lt;span&gt;&quot;google&quot;&lt;/span&gt;&lt;span&gt;,&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;175&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;        &lt;/span&gt;&lt;span&gt;&quot;format&quot;&lt;/span&gt;&lt;span&gt;: &lt;/span&gt;&lt;span&gt;&quot;binary&quot;&lt;/span&gt;&lt;span&gt;,&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;176&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;        &lt;/span&gt;&lt;span&gt;&quot;url&quot;&lt;/span&gt;&lt;span&gt;: &lt;/span&gt;&lt;span&gt;&quot;https://raw.githubusercontent.com/MetaCubeX/meta-rules-dat/sing/geo/geosite/google.srs&quot;&lt;/span&gt;&lt;span&gt;,&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;177&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;        &lt;/span&gt;&lt;span&gt;&quot;update_interval&quot;&lt;/span&gt;&lt;span&gt;: &lt;/span&gt;&lt;span&gt;&quot;7d&quot;&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;178&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;&lt;span&gt;      &lt;/span&gt;&lt;/span&gt;&lt;span&gt;},&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;179&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;&lt;span&gt;      &lt;/span&gt;&lt;/span&gt;&lt;span&gt;{&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;180&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;        &lt;/span&gt;&lt;span&gt;// 下面四个是这几家服务自己的 IP 段，用来兜底「只有 IP、没有域名」的连接&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;181&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;        &lt;/span&gt;&lt;span&gt;&quot;type&quot;&lt;/span&gt;&lt;span&gt;: &lt;/span&gt;&lt;span&gt;&quot;remote&quot;&lt;/span&gt;&lt;span&gt;,&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;182&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;        &lt;/span&gt;&lt;span&gt;&quot;tag&quot;&lt;/span&gt;&lt;span&gt;: &lt;/span&gt;&lt;span&gt;&quot;google-ip&quot;&lt;/span&gt;&lt;span&gt;,&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;183&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;        &lt;/span&gt;&lt;span&gt;&quot;format&quot;&lt;/span&gt;&lt;span&gt;: &lt;/span&gt;&lt;span&gt;&quot;binary&quot;&lt;/span&gt;&lt;span&gt;,&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;184&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;        &lt;/span&gt;&lt;span&gt;&quot;url&quot;&lt;/span&gt;&lt;span&gt;: &lt;/span&gt;&lt;span&gt;&quot;https://raw.githubusercontent.com/MetaCubeX/meta-rules-dat/sing/geo/geoip/google.srs&quot;&lt;/span&gt;&lt;span&gt;,&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;185&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;        &lt;/span&gt;&lt;span&gt;&quot;update_interval&quot;&lt;/span&gt;&lt;span&gt;: &lt;/span&gt;&lt;span&gt;&quot;7d&quot;&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;186&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;&lt;span&gt;      &lt;/span&gt;&lt;/span&gt;&lt;span&gt;},&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;187&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;&lt;span&gt;      &lt;/span&gt;&lt;/span&gt;&lt;span&gt;{&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;188&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;        &lt;/span&gt;&lt;span&gt;&quot;type&quot;&lt;/span&gt;&lt;span&gt;: &lt;/span&gt;&lt;span&gt;&quot;remote&quot;&lt;/span&gt;&lt;span&gt;,&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;189&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;        &lt;/span&gt;&lt;span&gt;&quot;tag&quot;&lt;/span&gt;&lt;span&gt;: &lt;/span&gt;&lt;span&gt;&quot;telegram-ip&quot;&lt;/span&gt;&lt;span&gt;,&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;190&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;        &lt;/span&gt;&lt;span&gt;&quot;format&quot;&lt;/span&gt;&lt;span&gt;: &lt;/span&gt;&lt;span&gt;&quot;binary&quot;&lt;/span&gt;&lt;span&gt;,&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;191&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;        &lt;/span&gt;&lt;span&gt;&quot;url&quot;&lt;/span&gt;&lt;span&gt;: &lt;/span&gt;&lt;span&gt;&quot;https://raw.githubusercontent.com/MetaCubeX/meta-rules-dat/sing/geo/geoip/telegram.srs&quot;&lt;/span&gt;&lt;span&gt;,&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;192&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;        &lt;/span&gt;&lt;span&gt;&quot;update_interval&quot;&lt;/span&gt;&lt;span&gt;: &lt;/span&gt;&lt;span&gt;&quot;7d&quot;&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;193&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;&lt;span&gt;      &lt;/span&gt;&lt;/span&gt;&lt;span&gt;},&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;194&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;&lt;span&gt;      &lt;/span&gt;&lt;/span&gt;&lt;span&gt;{&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;195&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;        &lt;/span&gt;&lt;span&gt;&quot;type&quot;&lt;/span&gt;&lt;span&gt;: &lt;/span&gt;&lt;span&gt;&quot;remote&quot;&lt;/span&gt;&lt;span&gt;,&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;196&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;        &lt;/span&gt;&lt;span&gt;&quot;tag&quot;&lt;/span&gt;&lt;span&gt;: &lt;/span&gt;&lt;span&gt;&quot;twitter-ip&quot;&lt;/span&gt;&lt;span&gt;,&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;197&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;        &lt;/span&gt;&lt;span&gt;&quot;format&quot;&lt;/span&gt;&lt;span&gt;: &lt;/span&gt;&lt;span&gt;&quot;binary&quot;&lt;/span&gt;&lt;span&gt;,&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;198&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;        &lt;/span&gt;&lt;span&gt;&quot;url&quot;&lt;/span&gt;&lt;span&gt;: &lt;/span&gt;&lt;span&gt;&quot;https://raw.githubusercontent.com/MetaCubeX/meta-rules-dat/sing/geo/geoip/twitter.srs&quot;&lt;/span&gt;&lt;span&gt;,&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;199&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;        &lt;/span&gt;&lt;span&gt;&quot;update_interval&quot;&lt;/span&gt;&lt;span&gt;: &lt;/span&gt;&lt;span&gt;&quot;7d&quot;&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;200&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;&lt;span&gt;      &lt;/span&gt;&lt;/span&gt;&lt;span&gt;},&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;201&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;&lt;span&gt;      &lt;/span&gt;&lt;/span&gt;&lt;span&gt;{&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;202&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;        &lt;/span&gt;&lt;span&gt;&quot;type&quot;&lt;/span&gt;&lt;span&gt;: &lt;/span&gt;&lt;span&gt;&quot;remote&quot;&lt;/span&gt;&lt;span&gt;,&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;203&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;        &lt;/span&gt;&lt;span&gt;&quot;tag&quot;&lt;/span&gt;&lt;span&gt;: &lt;/span&gt;&lt;span&gt;&quot;facebook-ip&quot;&lt;/span&gt;&lt;span&gt;,&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;204&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;        &lt;/span&gt;&lt;span&gt;&quot;format&quot;&lt;/span&gt;&lt;span&gt;: &lt;/span&gt;&lt;span&gt;&quot;binary&quot;&lt;/span&gt;&lt;span&gt;,&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;205&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;        &lt;/span&gt;&lt;span&gt;&quot;url&quot;&lt;/span&gt;&lt;span&gt;: &lt;/span&gt;&lt;span&gt;&quot;https://raw.githubusercontent.com/MetaCubeX/meta-rules-dat/sing/geo/geoip/facebook.srs&quot;&lt;/span&gt;&lt;span&gt;,&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;206&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;        &lt;/span&gt;&lt;span&gt;&quot;update_interval&quot;&lt;/span&gt;&lt;span&gt;: &lt;/span&gt;&lt;span&gt;&quot;7d&quot;&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;207&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;&lt;span&gt;      &lt;/span&gt;&lt;/span&gt;&lt;span&gt;},&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;208&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;&lt;span&gt;      &lt;/span&gt;&lt;/span&gt;&lt;span&gt;{&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;209&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;        &lt;/span&gt;&lt;span&gt;// 国内 IP 段，用在最后一条 QUIC 规则里&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;210&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;        &lt;/span&gt;&lt;span&gt;&quot;type&quot;&lt;/span&gt;&lt;span&gt;: &lt;/span&gt;&lt;span&gt;&quot;remote&quot;&lt;/span&gt;&lt;span&gt;,&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;211&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;        &lt;/span&gt;&lt;span&gt;&quot;tag&quot;&lt;/span&gt;&lt;span&gt;: &lt;/span&gt;&lt;span&gt;&quot;cn-ip&quot;&lt;/span&gt;&lt;span&gt;,&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;212&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;        &lt;/span&gt;&lt;span&gt;&quot;format&quot;&lt;/span&gt;&lt;span&gt;: &lt;/span&gt;&lt;span&gt;&quot;binary&quot;&lt;/span&gt;&lt;span&gt;,&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;213&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;        &lt;/span&gt;&lt;span&gt;&quot;url&quot;&lt;/span&gt;&lt;span&gt;: &lt;/span&gt;&lt;span&gt;&quot;https://raw.githubusercontent.com/MetaCubeX/meta-rules-dat/sing/geo/geoip/cn.srs&quot;&lt;/span&gt;&lt;span&gt;,&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;214&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;        &lt;/span&gt;&lt;span&gt;&quot;update_interval&quot;&lt;/span&gt;&lt;span&gt;: &lt;/span&gt;&lt;span&gt;&quot;7d&quot;&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;215&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;&lt;span&gt;      &lt;/span&gt;&lt;/span&gt;&lt;span&gt;}&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;216&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;&lt;span&gt;    &lt;/span&gt;&lt;/span&gt;&lt;span&gt;],&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;217&lt;/div&gt;&lt;/div&gt;&lt;div&gt;
&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;218&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;    &lt;/span&gt;&lt;span&gt;// ---------- 分流规则：从上往下匹配，命中第一条就停 ----------&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;219&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;    &lt;/span&gt;&lt;span&gt;&quot;rules&quot;&lt;/span&gt;&lt;span&gt;: [&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;220&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;&lt;span&gt;      &lt;/span&gt;&lt;/span&gt;&lt;span&gt;{&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;221&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;        &lt;/span&gt;&lt;span&gt;// 1. 嗅探：从 TLS、HTTP、QUIC 握手里读出域名&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;222&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;        &lt;/span&gt;&lt;span&gt;//    App 写死 IP、或浏览器自己用加密 DNS 时，靠它照样能按域名分流&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;223&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;        &lt;/span&gt;&lt;span&gt;&quot;action&quot;&lt;/span&gt;&lt;span&gt;: &lt;/span&gt;&lt;span&gt;&quot;sniff&quot;&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;224&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;&lt;span&gt;      &lt;/span&gt;&lt;/span&gt;&lt;span&gt;},&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;225&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;&lt;span&gt;      &lt;/span&gt;&lt;/span&gt;&lt;span&gt;{&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;226&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;        &lt;/span&gt;&lt;span&gt;// 2. 接管所有 DNS 查询，交给上面的 DNS 规则处理&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;227&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;        &lt;/span&gt;&lt;span&gt;&quot;protocol&quot;&lt;/span&gt;&lt;span&gt;: &lt;/span&gt;&lt;span&gt;&quot;dns&quot;&lt;/span&gt;&lt;span&gt;,&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;228&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;        &lt;/span&gt;&lt;span&gt;&quot;action&quot;&lt;/span&gt;&lt;span&gt;: &lt;/span&gt;&lt;span&gt;&quot;hijack-dns&quot;&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;229&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;&lt;span&gt;      &lt;/span&gt;&lt;/span&gt;&lt;span&gt;},&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;230&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;&lt;span&gt;      &lt;/span&gt;&lt;/span&gt;&lt;span&gt;{&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;231&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;        &lt;/span&gt;&lt;span&gt;// 3. 局域网地址走 Wi-Fi，不然路由器后台、打印机都访问不了&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;232&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;        &lt;/span&gt;&lt;span&gt;&quot;ip_is_private&quot;&lt;/span&gt;&lt;span&gt;: &lt;/span&gt;&lt;span&gt;true&lt;/span&gt;&lt;span&gt;,&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;233&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;        &lt;/span&gt;&lt;span&gt;&quot;action&quot;&lt;/span&gt;&lt;span&gt;: &lt;/span&gt;&lt;span&gt;&quot;route&quot;&lt;/span&gt;&lt;span&gt;,&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;234&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;        &lt;/span&gt;&lt;span&gt;&quot;outbound&quot;&lt;/span&gt;&lt;span&gt;: &lt;/span&gt;&lt;span&gt;&quot;default&quot;&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;235&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;&lt;span&gt;      &lt;/span&gt;&lt;/span&gt;&lt;span&gt;},&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;236&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;&lt;span&gt;      &lt;/span&gt;&lt;/span&gt;&lt;span&gt;{&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;237&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;        &lt;/span&gt;&lt;span&gt;// 4. Claude、ChatGPT、境外网站、GitHub、Google 走 eSIM&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;238&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;        &lt;/span&gt;&lt;span&gt;//    Steam 不用单独写：商店和社区在境外网站列表里，下载域名不在任何列表里，会走 Wi-Fi&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;239&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;        &lt;/span&gt;&lt;span&gt;&quot;rule_set&quot;&lt;/span&gt;&lt;span&gt;: [&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;240&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;          &lt;/span&gt;&lt;span&gt;&quot;anthropic&quot;&lt;/span&gt;&lt;span&gt;,&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;241&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;          &lt;/span&gt;&lt;span&gt;&quot;openai&quot;&lt;/span&gt;&lt;span&gt;,&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;242&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;          &lt;/span&gt;&lt;span&gt;&quot;overseas-list&quot;&lt;/span&gt;&lt;span&gt;,&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;243&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;          &lt;/span&gt;&lt;span&gt;&quot;github&quot;&lt;/span&gt;&lt;span&gt;,&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;244&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;          &lt;/span&gt;&lt;span&gt;&quot;google&quot;&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;245&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;&lt;span&gt;        &lt;/span&gt;&lt;/span&gt;&lt;span&gt;],&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;246&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;        &lt;/span&gt;&lt;span&gt;&quot;action&quot;&lt;/span&gt;&lt;span&gt;: &lt;/span&gt;&lt;span&gt;&quot;route&quot;&lt;/span&gt;&lt;span&gt;,&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;247&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;        &lt;/span&gt;&lt;span&gt;&quot;outbound&quot;&lt;/span&gt;&lt;span&gt;: &lt;/span&gt;&lt;span&gt;&quot;esim&quot;&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;248&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;&lt;span&gt;      &lt;/span&gt;&lt;/span&gt;&lt;span&gt;},&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;249&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;&lt;span&gt;      &lt;/span&gt;&lt;/span&gt;&lt;span&gt;{&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;250&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;        &lt;/span&gt;&lt;span&gt;// 5. 只有 IP 的连接：按这几家服务的 IP 段兜底，也走 eSIM&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;251&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;        &lt;/span&gt;&lt;span&gt;&quot;rule_set&quot;&lt;/span&gt;&lt;span&gt;: [&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;252&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;          &lt;/span&gt;&lt;span&gt;&quot;google-ip&quot;&lt;/span&gt;&lt;span&gt;,&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;253&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;          &lt;/span&gt;&lt;span&gt;&quot;telegram-ip&quot;&lt;/span&gt;&lt;span&gt;,&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;254&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;          &lt;/span&gt;&lt;span&gt;&quot;twitter-ip&quot;&lt;/span&gt;&lt;span&gt;,&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;255&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;          &lt;/span&gt;&lt;span&gt;&quot;facebook-ip&quot;&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;256&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;&lt;span&gt;        &lt;/span&gt;&lt;/span&gt;&lt;span&gt;],&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;257&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;        &lt;/span&gt;&lt;span&gt;&quot;action&quot;&lt;/span&gt;&lt;span&gt;: &lt;/span&gt;&lt;span&gt;&quot;route&quot;&lt;/span&gt;&lt;span&gt;,&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;258&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;        &lt;/span&gt;&lt;span&gt;&quot;outbound&quot;&lt;/span&gt;&lt;span&gt;: &lt;/span&gt;&lt;span&gt;&quot;esim&quot;&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;259&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;&lt;span&gt;      &lt;/span&gt;&lt;/span&gt;&lt;span&gt;},&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;260&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;&lt;span&gt;      &lt;/span&gt;&lt;/span&gt;&lt;span&gt;{&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;261&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;        &lt;/span&gt;&lt;span&gt;// 6. 发往境外 IP 的 QUIC（HTTP/3，UDP 443）一律拒绝&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;262&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;        &lt;/span&gt;&lt;span&gt;//    QUIC 有时读不出域名，会漏到 Wi-Fi；拒绝后 App 会立刻改用 TCP，&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;263&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;        &lt;/span&gt;&lt;span&gt;//    TCP 能读出域名，就能按上面的规则分流。国内 IP 的 QUIC 不受影响&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;264&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;        &lt;/span&gt;&lt;span&gt;&quot;type&quot;&lt;/span&gt;&lt;span&gt;: &lt;/span&gt;&lt;span&gt;&quot;logical&quot;&lt;/span&gt;&lt;span&gt;,&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;265&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;        &lt;/span&gt;&lt;span&gt;&quot;mode&quot;&lt;/span&gt;&lt;span&gt;: &lt;/span&gt;&lt;span&gt;&quot;and&quot;&lt;/span&gt;&lt;span&gt;,&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;266&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;        &lt;/span&gt;&lt;span&gt;&quot;rules&quot;&lt;/span&gt;&lt;span&gt;: [&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;267&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;&lt;span&gt;          &lt;/span&gt;&lt;/span&gt;&lt;span&gt;{&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;268&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;            &lt;/span&gt;&lt;span&gt;&quot;network&quot;&lt;/span&gt;&lt;span&gt;: &lt;/span&gt;&lt;span&gt;&quot;udp&quot;&lt;/span&gt;&lt;span&gt;,&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;269&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;            &lt;/span&gt;&lt;span&gt;&quot;port&quot;&lt;/span&gt;&lt;span&gt;: &lt;/span&gt;&lt;span&gt;443&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;270&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;&lt;span&gt;          &lt;/span&gt;&lt;/span&gt;&lt;span&gt;},&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;271&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;&lt;span&gt;          &lt;/span&gt;&lt;/span&gt;&lt;span&gt;{&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;272&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;            &lt;/span&gt;&lt;span&gt;&quot;rule_set&quot;&lt;/span&gt;&lt;span&gt;: &lt;/span&gt;&lt;span&gt;&quot;cn-ip&quot;&lt;/span&gt;&lt;span&gt;,&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;273&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;            &lt;/span&gt;&lt;span&gt;&quot;invert&quot;&lt;/span&gt;&lt;span&gt;: &lt;/span&gt;&lt;span&gt;true&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;274&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;&lt;span&gt;          &lt;/span&gt;&lt;/span&gt;&lt;span&gt;}&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;275&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;&lt;span&gt;        &lt;/span&gt;&lt;/span&gt;&lt;span&gt;],&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;276&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;        &lt;/span&gt;&lt;span&gt;&quot;action&quot;&lt;/span&gt;&lt;span&gt;: &lt;/span&gt;&lt;span&gt;&quot;reject&quot;&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;277&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;&lt;span&gt;      &lt;/span&gt;&lt;/span&gt;&lt;span&gt;}&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;278&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;&lt;span&gt;    &lt;/span&gt;&lt;/span&gt;&lt;span&gt;],&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;279&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;    &lt;/span&gt;&lt;span&gt;// 7. 其他所有流量走 Wi-Fi&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;280&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;    &lt;/span&gt;&lt;span&gt;&quot;final&quot;&lt;/span&gt;&lt;span&gt;: &lt;/span&gt;&lt;span&gt;&quot;default&quot;&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;281&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;&lt;span&gt;  &lt;/span&gt;&lt;/span&gt;&lt;span&gt;},&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;282&lt;/div&gt;&lt;/div&gt;&lt;div&gt;
&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;283&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;  &lt;/span&gt;&lt;span&gt;// ---------- 其他 ----------&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;284&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;  &lt;/span&gt;&lt;span&gt;&quot;experimental&quot;&lt;/span&gt;&lt;span&gt;: {&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;285&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;    &lt;/span&gt;&lt;span&gt;// 把规则集等缓存到本地，重启后不用重新下载&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;286&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;    &lt;/span&gt;&lt;span&gt;&quot;cache_file&quot;&lt;/span&gt;&lt;span&gt;: {&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;287&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;      &lt;/span&gt;&lt;span&gt;&quot;enabled&quot;&lt;/span&gt;&lt;span&gt;: &lt;/span&gt;&lt;span&gt;true&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;288&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;&lt;span&gt;    &lt;/span&gt;&lt;/span&gt;&lt;span&gt;}&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;289&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;&lt;span&gt;  &lt;/span&gt;&lt;/span&gt;&lt;span&gt;}&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;290&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;}&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;/code&gt;&lt;/pre&gt;&lt;div&gt;&lt;div&gt;&lt;/div&gt;&lt;div&gt;&lt;/div&gt;&lt;/div&gt;&lt;/figure&gt;&lt;div&gt;&lt;/div&gt;&lt;/div&gt;&lt;span&gt;展开&lt;/span&gt;&lt;span&gt;收起&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;section&gt;&lt;h3&gt;配置解析&lt;a href=&quot;#配置解析&quot;&gt;&lt;span&gt;#&lt;/span&gt;&lt;/a&gt;&lt;/h3&gt;&lt;p&gt;&lt;strong&gt;出口（&lt;code&gt;outbounds&lt;/code&gt;）&lt;/strong&gt;&lt;/p&gt;&lt;ul&gt;
&lt;li&gt;&lt;code&gt;default&lt;/code&gt;：普通直连，跟随系统默认网络。&lt;/li&gt;
&lt;li&gt;&lt;code&gt;esim&lt;/code&gt;：直连，用 &lt;code&gt;network_type&lt;/code&gt; 限定只走蜂窝或有线网卡，原理见第四节。&lt;code&gt;network_strategy: default&lt;/code&gt; 表示只用这两类网络，不会在它们不可用时偷偷改走 Wi-Fi。&lt;/li&gt;
&lt;/ul&gt;&lt;p&gt;&lt;strong&gt;DNS（&lt;code&gt;dns&lt;/code&gt;）&lt;/strong&gt;&lt;/p&gt;&lt;ul&gt;
&lt;li&gt;&lt;code&gt;dns-cn&lt;/code&gt;：阿里的加密 DNS，走默认网络，解析国内网站。&lt;/li&gt;
&lt;li&gt;&lt;code&gt;dns-esim&lt;/code&gt;：Cloudflare 的加密 DNS，经 &lt;code&gt;esim&lt;/code&gt; 出口发出，专门解析要走 eSIM 的域名，保证拿到境外网络视角下可用的地址。&lt;/li&gt;
&lt;li&gt;第一条规则让&lt;strong&gt;要走 eSIM 的域名只返回 IPv4 地址&lt;/strong&gt;（拦掉 &lt;code&gt;AAAA&lt;/code&gt; 查询）。境外 eSIM 漫游时只有 IPv4，拿到 IPv6 地址就连不上；国内网站不受影响，照常可以用 IPv6。为什么这条很重要，见第九节。&lt;/li&gt;
&lt;/ul&gt;&lt;p&gt;&lt;strong&gt;规则集（&lt;code&gt;route.rule_set&lt;/code&gt;）&lt;/strong&gt;&lt;/p&gt;&lt;p&gt;全部来自 &lt;a href=&quot;https://github.com/MetaCubeX/meta-rules-dat&quot; target=&quot;_blank&quot;&gt;MetaCubeX/meta-rules-dat&lt;/a&gt; 项目的 &lt;code&gt;sing&lt;/code&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;th&gt;内容&lt;/th&gt;&lt;/tr&gt;&lt;/thead&gt;&lt;tbody&gt;&lt;tr&gt;&lt;td&gt;&lt;code&gt;anthropic&lt;/code&gt;&lt;/td&gt;&lt;td&gt;&lt;code&gt;geo/geosite/anthropic.srs&lt;/code&gt;&lt;/td&gt;&lt;td&gt;Claude / Anthropic 的全部域名，包括 Artifact 用的 &lt;code&gt;claudeusercontent.com&lt;/code&gt;&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;&lt;code&gt;openai&lt;/code&gt;&lt;/td&gt;&lt;td&gt;&lt;code&gt;geo/geosite/openai.srs&lt;/code&gt;&lt;/td&gt;&lt;td&gt;ChatGPT / OpenAI 的全部域名，包括静态资源域名&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;&lt;code&gt;overseas-list&lt;/code&gt;&lt;/td&gt;&lt;td&gt;&lt;code&gt;geo/geosite/gfw.srs&lt;/code&gt;&lt;/td&gt;&lt;td&gt;常用境外网站域名（上游文件名沿用了项目里的原名），Steam 商店和社区、Telegram、X、YouTube、Grok 等都在里面&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;&lt;code&gt;github&lt;/code&gt;&lt;/td&gt;&lt;td&gt;&lt;code&gt;geo/geosite/github.srs&lt;/code&gt;&lt;/td&gt;&lt;td&gt;GitHub 全部域名&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;&lt;code&gt;google&lt;/code&gt;&lt;/td&gt;&lt;td&gt;&lt;code&gt;geo/geosite/google.srs&lt;/code&gt;&lt;/td&gt;&lt;td&gt;Google 全部域名，包括 Gemini&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;&lt;code&gt;google-ip&lt;/code&gt;、&lt;code&gt;telegram-ip&lt;/code&gt;、&lt;code&gt;twitter-ip&lt;/code&gt;、&lt;code&gt;facebook-ip&lt;/code&gt;&lt;/td&gt;&lt;td&gt;&lt;code&gt;geo/geoip/&lt;/code&gt; 下同名文件&lt;/td&gt;&lt;td&gt;这几家服务自己的 IP 段&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;&lt;code&gt;cn-ip&lt;/code&gt;&lt;/td&gt;&lt;td&gt;&lt;code&gt;geo/geoip/cn.srs&lt;/code&gt;&lt;/td&gt;&lt;td&gt;国内 IP 段&lt;/td&gt;&lt;/tr&gt;&lt;/tbody&gt;&lt;/table&gt;&lt;p&gt;&lt;code&gt;http_clients&lt;/code&gt; 定义了一个经 &lt;code&gt;esim&lt;/code&gt; 出口下载的客户端，&lt;code&gt;route.default_http_client&lt;/code&gt; 让所有规则集都用它下载，因为规则集托管在 GitHub 上。每 7 天更新一次（&lt;code&gt;update_interval: 7d&lt;/code&gt;），列表变化很慢，没必要天天下载、浪费 eSIM 流量。&lt;/p&gt;&lt;p&gt;&lt;strong&gt;分流规则（&lt;code&gt;route.rules&lt;/code&gt;）&lt;/strong&gt;&lt;/p&gt;&lt;p&gt;规则&lt;strong&gt;从上往下匹配，命中第一条就停&lt;/strong&gt;：&lt;/p&gt;&lt;ol&gt;
&lt;li&gt;&lt;code&gt;sniff&lt;/code&gt;：从 TLS、HTTP、QUIC 握手里读出域名。浏览器自己用加密 DNS 解析、或者 App 写死了 IP 时，靠它照样能按域名分流。&lt;/li&gt;
&lt;li&gt;&lt;code&gt;hijack-dns&lt;/code&gt;：接管所有 DNS 查询，交给上面的 DNS 规则处理。&lt;/li&gt;
&lt;li&gt;局域网地址走默认网络，不然路由器后台、打印机都访问不了。&lt;/li&gt;
&lt;li&gt;Claude、ChatGPT、境外网站列表、GitHub、Google 走 &lt;code&gt;esim&lt;/code&gt;。&lt;strong&gt;Steam 不用单独写&lt;/strong&gt;：商店和社区的域名已经在境外网站列表里，下载用的 &lt;code&gt;steamcontent.com&lt;/code&gt; 和国内下载节点不在任何列表里，会落到最后走 Wi-Fi。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;只有 IP 的连接按 IP 段兜底。&lt;/strong&gt; Google、Telegram、Twitter（X）、Facebook 这几家有时会绕过域名、直接用 IP 连接，按它们自己的网段走 &lt;code&gt;esim&lt;/code&gt;。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;发往境外 IP 的 QUIC 一律拒绝。&lt;/strong&gt; 原因见第九节。&lt;/li&gt;
&lt;li&gt;其他所有流量走 &lt;code&gt;default&lt;/code&gt;（&lt;code&gt;final&lt;/code&gt;）。&lt;/li&gt;
&lt;/ol&gt;&lt;/section&gt;&lt;/section&gt;
&lt;section&gt;&lt;h2&gt;六、Mac 上的设置&lt;a href=&quot;#六mac-上的设置&quot;&gt;&lt;span&gt;#&lt;/span&gt;&lt;/a&gt;&lt;/h2&gt;&lt;section&gt;&lt;h3&gt;安装&lt;a href=&quot;#安装&quot;&gt;&lt;span&gt;#&lt;/span&gt;&lt;/a&gt;&lt;/h3&gt;&lt;p&gt;sing-box 的 Mac 版在 App Store 里叫 sing-box，&lt;a href=&quot;https://sing-box.sagernet.org/clients/apple/&quot; target=&quot;_blank&quot;&gt;官方文档&lt;/a&gt;说需要中国大陆以外的 Apple 账户。我用外区账号也提示「所在国家尚不提供此 App」，最后用 Homebrew 装了官方的独立版（SFM），功能一样，也能读 iCloud 配置：&lt;/p&gt;&lt;div&gt;&lt;figure&gt;&lt;figcaption&gt;&lt;span&gt;&lt;/span&gt;&lt;span&gt;Terminal window&lt;/span&gt;&lt;/figcaption&gt;&lt;pre&gt;&lt;code&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;1&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;brew&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;install&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;--cask&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;sfm&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;/code&gt;&lt;/pre&gt;&lt;div&gt;&lt;div&gt;&lt;/div&gt;&lt;div&gt;&lt;/div&gt;&lt;/div&gt;&lt;/figure&gt;&lt;/div&gt;&lt;p&gt;注意是 &lt;code&gt;--cask sfm&lt;/code&gt;。&lt;code&gt;brew install sing-box&lt;/code&gt; 装的是命令行版，不支持 &lt;code&gt;network_type&lt;/code&gt;，也读不了 iCloud 配置。&lt;/p&gt;&lt;p&gt;第一次启动时按提示允许安装网络扩展。如果系统提示「系统扩展已被阻止」，去「系统设置 → 隐私与安全性」点「允许」，之后再允许添加 VPN 配置。&lt;/p&gt;&lt;/section&gt;&lt;section&gt;&lt;h3&gt;网络设置&lt;a href=&quot;#网络设置&quot;&gt;&lt;span&gt;#&lt;/span&gt;&lt;/a&gt;&lt;/h3&gt;&lt;ol&gt;
&lt;li&gt;&lt;strong&gt;Wi-Fi 连家里路由器&lt;/strong&gt;，并在「系统设置 → Wi-Fi」里关掉 iPhone 热点的「自动加入」，防止 Mac 自己连到热点上。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;iPhone 数据卡设成 eSIM&lt;/strong&gt;，用数据线连 Mac，打开「个人热点 → 允许其他人加入」，第一次连接时在手机上点「信任」。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;关掉 iPhone USB 的「需要时启用」&lt;/strong&gt;：「系统设置 → 网络 → iPhone USB → 详细信息」。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;服务顺序&lt;/strong&gt;：Wi-Fi 排在 iPhone USB 上面。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Wi-Fi 的 IPv6 设成「自动」&lt;/strong&gt;：「系统设置 → 网络 → Wi-Fi → 详细信息 → TCP/IP」，「配置 IPv6」选「自动」。如果之前为了别的代理工具改成过「仅本地链接」，国内网站会全部打不开，原因见第九节。&lt;/li&gt;
&lt;/ol&gt;&lt;/section&gt;&lt;section&gt;&lt;h3&gt;导入配置&lt;a href=&quot;#导入配置&quot;&gt;&lt;span&gt;#&lt;/span&gt;&lt;/a&gt;&lt;/h3&gt;&lt;p&gt;在 sing-box 里新建配置，类型选「iCloud」，选中 &lt;code&gt;esim-split.json&lt;/code&gt;，回到首页启动。&lt;/p&gt;&lt;p&gt;如果之前装过 Clash Verge Rev 之类的工具，先关掉它的虚拟网卡和系统代理并退出，两个软件同时接管流量会互相干扰。&lt;/p&gt;&lt;/section&gt;&lt;/section&gt;
&lt;section&gt;&lt;h2&gt;七、iPhone 上的设置&lt;a href=&quot;#七iphone-上的设置&quot;&gt;&lt;span&gt;#&lt;/span&gt;&lt;/a&gt;&lt;/h2&gt;&lt;section&gt;&lt;h3&gt;装哪个 App&lt;a href=&quot;#装哪个-app&quot;&gt;&lt;span&gt;#&lt;/span&gt;&lt;/a&gt;&lt;/h3&gt;&lt;p&gt;sing-box 的 iOS 客户端在国区 App Store 搜不到，需要外区 Apple ID，App 本身免费。要装的是&lt;a href=&quot;https://sing-box.sagernet.org/clients/apple/&quot; target=&quot;_blank&quot;&gt;官方文档里链接的那个&lt;/a&gt;（App Store 上叫 sing-box MT）。&lt;/p&gt;&lt;p&gt;在 App Store 搜「sing-box」时排在前面的「sing-box VT」&lt;strong&gt;不是官方的&lt;/strong&gt;：开发商是另一家公司，页面上自称「又一个发行版」，内核还停在 1.11。这份配置用的是 1.12 以后的 DNS 写法，在它上面会直接报错。&lt;/p&gt;&lt;/section&gt;&lt;section&gt;&lt;h3&gt;设置步骤&lt;a href=&quot;#设置步骤&quot;&gt;&lt;span&gt;#&lt;/span&gt;&lt;/a&gt;&lt;/h3&gt;&lt;ol&gt;
&lt;li&gt;&lt;strong&gt;导入配置&lt;/strong&gt;：打开 sing-box →「配置」→ 新建 → 类型选「iCloud」→ 选 &lt;code&gt;esim-split.json&lt;/code&gt;。回到「仪表」页面选中这个配置，打开开关。第一次会请求添加 VPN 配置，点允许。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;允许 sing-box 使用蜂窝数据&lt;/strong&gt;：「设置 → 蜂窝网络」，往下翻到 sing-box，打开开关。不打开的话，&lt;code&gt;esim&lt;/code&gt; 出口走不了蜂窝。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;在家时数据卡设成 eSIM&lt;/strong&gt;：「设置 → 蜂窝网络 → 蜂窝数据」选 eSIM，并关掉同一页底部的「允许切换蜂窝数据」。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;关掉「无线局域网助理」&lt;/strong&gt;：「设置 → 蜂窝网络」拉到最底部。它会在 Wi-Fi 信号差时自动改用蜂窝，在家时蜂窝就是 eSIM。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;给 eSIM 打开「低数据模式」&lt;/strong&gt;：「设置 → 蜂窝网络 →」点 eSIM 线路。系统会暂停照片同步、自动更新这类后台任务，只影响蜂窝，连着 Wi-Fi 时照常进行。&lt;/li&gt;
&lt;/ol&gt;&lt;p&gt;出门后把数据卡切回主号，这时两个出口都只剩蜂窝，sing-box 开着关着效果一样。&lt;/p&gt;&lt;/section&gt;&lt;section&gt;&lt;h3&gt;连上 Wi-Fi 自动切卡、自动开 sing-box&lt;a href=&quot;#连上-wi-fi-自动切卡自动开-sing-box&quot;&gt;&lt;span&gt;#&lt;/span&gt;&lt;/a&gt;&lt;/h3&gt;&lt;p&gt;用「快捷指令」的自动化就能做到，连上 Wi-Fi 时自动把数据卡切到 eSIM，并打开 sing-box：&lt;/p&gt;&lt;ol&gt;
&lt;li&gt;「快捷指令」→「自动化」→「+」→「无线局域网」，网络选家里的 Wi-Fi，勾选「加入」，选「立即运行」。&lt;/li&gt;
&lt;li&gt;添加操作「&lt;strong&gt;设定默认号码&lt;/strong&gt;」，显示为「将默认 数据 号码设为 …」：把「数据」保持不变，号码选 eSIM。注意不要选成「语音」，那是打电话用的默认号码。&lt;/li&gt;
&lt;li&gt;添加操作「&lt;strong&gt;设定 VPN&lt;/strong&gt;」，显示为「连接 SFI」（SFI 是 sing-box 的 iOS 客户端在系统里的名字）。&lt;/li&gt;
&lt;/ol&gt;&lt;p&gt;要在离开时自动关掉，再建一个自动化：同样选家里的 Wi-Fi，勾选「离开」，操作选「设定 VPN」→「断开连接」。想顺便切回主号，再加一个「设定默认号码」，号码选主号。&lt;/p&gt;&lt;p&gt;几个细节：&lt;/p&gt;&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;网络最好选家里的 Wi-Fi，而不是「任何网络」。&lt;/strong&gt; 选「任何网络」的话，在外面连公司、咖啡店的 Wi-Fi 也会切到 eSIM。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;别同时打开 sing-box 自己的「按需启动」。&lt;/strong&gt; 它的作用一般是「断了自动重连」，会和「离开时断开」打架。&lt;/li&gt;
&lt;li&gt;网上很多文章说 iOS 14.5 起快捷指令就能切换数据卡，我核对后发现那次加的是「设定语音与数据」，只能切换 4G/5G 模式，不能换卡。实际能换卡的是「设定默认号码」这个操作，以你手机里实际能搜到的为准。&lt;/li&gt;
&lt;/ul&gt;&lt;/section&gt;&lt;/section&gt;
&lt;section&gt;&lt;h2&gt;八、用 iCloud 同步配置&lt;a href=&quot;#八用-icloud-同步配置&quot;&gt;&lt;span&gt;#&lt;/span&gt;&lt;/a&gt;&lt;/h2&gt;&lt;p&gt;两台设备读的都是「iCloud 云盘 → sing-box」里的 &lt;code&gt;esim-split.json&lt;/code&gt;。在 Mac 上用任意编辑器改完保存即可，但有两点要注意：&lt;/p&gt;&lt;ol&gt;
&lt;li&gt;&lt;strong&gt;sing-box 不会自动重新加载。&lt;/strong&gt; 它只在启动时读一次配置，改完后要在 App 里把开关关掉再打开。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;iPhone 要先收到新文件。&lt;/strong&gt; iCloud 同步有时要等几秒到几分钟。想快一点，就在 iPhone 的「文件」App 里打开「iCloud 云盘 → sing-box」，点一下这个文件，它会立刻下载最新版，然后再重启 sing-box。&lt;/li&gt;
&lt;/ol&gt;&lt;p&gt;改错了也不怕：配置有语法错误时，sing-box 启动会直接报错，不会带着错误配置悄悄运行。&lt;/p&gt;&lt;p&gt;另外，刚重启或者刚加了新规则集时，sing-box 要先经 eSIM 下载规则集，这几十秒里对应的网站可能打不开，等一会儿就好。&lt;/p&gt;&lt;/section&gt;
&lt;section&gt;&lt;h2&gt;九、一路补过的漏洞&lt;a href=&quot;#九一路补过的漏洞&quot;&gt;&lt;span&gt;#&lt;/span&gt;&lt;/a&gt;&lt;/h2&gt;&lt;p&gt;这套配置不是一次写成的。下面这些坑，有的是实际用的时候在日志和「连接」页面里发现的，有的是专门回头检查「会不会悄悄打不开」和「会不会悄悄用掉 eSIM 流量」时找到的。&lt;/p&gt;&lt;section&gt;&lt;h3&gt;Mac 连接上的坑&lt;a href=&quot;#mac-连接上的坑&quot;&gt;&lt;span&gt;#&lt;/span&gt;&lt;/a&gt;&lt;/h3&gt;&lt;p&gt;&lt;strong&gt;Wi-Fi 连的就是热点。&lt;/strong&gt; 一开始 Mac 的 Wi-Fi 也连着 iPhone 热点，所有流量都在走热点，没有东西可分。记得在「系统设置 → Wi-Fi」里把 iPhone 热点的「自动加入」关掉，不然 Mac 会自己连回去。&lt;/p&gt;&lt;p&gt;&lt;strong&gt;iPhone USB 显示「已连接」，却拿不到 IP。&lt;/strong&gt; 用 &lt;code&gt;ifconfig en6&lt;/code&gt; 一看，状态是 &lt;code&gt;inactive&lt;/code&gt;。原因是「系统设置 → 网络 → iPhone USB → 详细信息」里的&lt;strong&gt;「需要时启用」&lt;/strong&gt;开着（英文版叫 Disable unless needed）：只要还有 Wi-Fi 可用，系统就把 iPhone USB 停掉，左边的状态点是黄色的。关掉之后状态点变绿，马上拿到了 &lt;code&gt;172.20.10.x&lt;/code&gt;。这是整个过程里最隐蔽的一个坑。&lt;/p&gt;&lt;p&gt;&lt;strong&gt;iPhone 开热点后自己断开了 Wi-Fi。&lt;/strong&gt; iPhone 打开个人热点后，通常会断开它自己连着的 Wi-Fi，因为热点要占用 Wi-Fi 芯片。这时 iPhone 上所有 App 都改走蜂窝数据，而数据卡正是 eSIM，微信、视频这些国内流量也会消耗 eSIM。所以 Mac 用热点时手机尽量少用，用完就关热点，iPhone 会自动连回 Wi-Fi。&lt;/p&gt;&lt;/section&gt;&lt;section&gt;&lt;h3&gt;规则上的漏洞&lt;a href=&quot;#规则上的漏洞&quot;&gt;&lt;span&gt;#&lt;/span&gt;&lt;/a&gt;&lt;/h3&gt;&lt;p&gt;&lt;strong&gt;Claude 的 Artifact 有时打不开。&lt;/strong&gt; 最初 Claude 的规则是手写的 &lt;code&gt;claude.ai&lt;/code&gt;、&lt;code&gt;claude.com&lt;/code&gt;、&lt;code&gt;anthropic.com&lt;/code&gt; 三个域名，而 Artifact（Claude 生成的网页）运行在独立的域名 &lt;code&gt;claudeusercontent.com&lt;/code&gt; 下。查了社区项目 &lt;a href=&quot;https://github.com/v2fly/domain-list-community&quot; target=&quot;_blank&quot;&gt;v2fly/domain-list-community&lt;/a&gt; 的 &lt;code&gt;data/anthropic&lt;/code&gt;，里面已经收录了 Anthropic 的十几个域名，于是改用这个社区列表，以后 Claude 启用新域名也会自动跟进。ChatGPT 同理，境外网站列表里只有主域名，没有 &lt;code&gt;oaistatic.com&lt;/code&gt; 这类静态资源域名，加了社区的 &lt;code&gt;openai&lt;/code&gt; 列表。&lt;/p&gt;&lt;p&gt;&lt;strong&gt;Steam 的规则写了一大串，其实一条都不需要。&lt;/strong&gt; Clash 时期我写了十几条 Steam 规则：先把下载、客户端更新、国内下载节点挑出来走 Wi-Fi，再让商店走热点。之所以这么麻烦，是因为用了 &lt;code&gt;steampowered.com&lt;/code&gt; 这种宽泛的写法，会把 &lt;code&gt;client-download.steampowered.com&lt;/code&gt; 这类下载域名也带去走热点。后来对照了境外网站列表，它已经精确收录了商店和社区的子域名（&lt;code&gt;store.steampowered.com&lt;/code&gt;、&lt;code&gt;steamcommunity.com&lt;/code&gt;、&lt;code&gt;steamstatic.com&lt;/code&gt; 等），下载域名一个都不在里面。什么都不写，结果就是「商店走 eSIM、下载走 Wi-Fi」，和游戏加速器的「商店加速」是一个效果。唯一的代价是商店里的预告片视频也会走 eSIM。&lt;/p&gt;&lt;p&gt;&lt;strong&gt;QUIC 连接识别不出域名。&lt;/strong&gt; iPhone 的连接页面里有一条 &lt;code&gt;UDP 64.233.170.101:443&lt;/code&gt; 走了 Wi-Fi，下载量为 0，30 秒后超时。这是 Google 的 IP，用的是 QUIC（HTTP/3）。这种新协议的域名有时读不出来，连接只剩一个 IP，所有域名规则都失效，落到了默认出口。App 发现 QUIC 连不上，一般会退回普通的 TCP，所以最终能用，只是会慢几秒。&lt;/p&gt;&lt;p&gt;与其追着整理 IP 段，不如&lt;strong&gt;从根上把这类连接变成能识别域名的连接&lt;/strong&gt;：发往境外 IP 的 QUIC 一律拒绝。App 会立刻改用 TCP，这是所有浏览器和主流 App 都有的标准行为，几乎没有延迟；TCP 连接能读出域名，自然就按列表分流了。国内 IP 的 QUIC 不受影响。另外再按 Google、Telegram、Twitter、Facebook 这几家自己的 IP 段兜底，覆盖那些直接用 IP 连接的情况。&lt;/p&gt;&lt;p&gt;&lt;strong&gt;换成 sing-box 后，Mac 上国内网站全部打不开。&lt;/strong&gt; Google 这类境外网站正常，国内网站在浏览器里一个都打不开，可 &lt;code&gt;nslookup baidu.com&lt;/code&gt; 能解析，&lt;code&gt;curl https://www.baidu.com&lt;/code&gt; 也返回 200。看 sing-box 日志，满屏都是这样的报错：&lt;/p&gt;&lt;div&gt;&lt;figure&gt;&lt;figcaption&gt;&lt;/figcaption&gt;&lt;pre&gt;&lt;code&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;1&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;connection: open connection to [240e:83:205:381:0:ff:b00f:96a2]:443&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;2&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;using outbound/direct[default]: dial en0 (8): dial tcp [...]: connect: no route to host&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;/code&gt;&lt;/pre&gt;&lt;div&gt;&lt;div&gt;&lt;/div&gt;&lt;div&gt;&lt;/div&gt;&lt;/div&gt;&lt;/figure&gt;&lt;/div&gt;&lt;p&gt;&lt;code&gt;240e:&lt;/code&gt; 开头的是国内运营商的 IPv6 地址。国内大网站基本都有 IPv6 地址，浏览器优先用 IPv6 去连，而 Mac 的 Wi-Fi（&lt;code&gt;en0&lt;/code&gt;）上根本没有 IPv6 线路，连不上，浏览器也没有退回 IPv4；&lt;code&gt;curl&lt;/code&gt; 失败后会马上换 IPv4，所以它能成功。&lt;/p&gt;&lt;p&gt;我一度以为是家里宽带没有 IPv6，在配置里把 IPv6 全部禁掉（DNS 只返回 IPv4 地址、拒绝所有 IPv6 连接），国内网站确实恢复了。但同一个 Wi-Fi 下 iPhone 走 IPv6 完全正常，说明宽带是有 IPv6 的。最后查到原因在 Mac 自己：用 Clash 时，为了防止少数 App 走 IPv6 绕过 Clash，我把 Mac Wi-Fi 的 IPv6 设成了「仅本地链接」，换成 sing-box 后忘了改回来。&lt;/p&gt;&lt;div&gt;&lt;figure&gt;&lt;figcaption&gt;&lt;span&gt;&lt;/span&gt;&lt;span&gt;Terminal window&lt;/span&gt;&lt;/figcaption&gt;&lt;pre&gt;&lt;code&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;1&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;networksetup&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;-getinfo&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;Wi-Fi&lt;/span&gt;&lt;span&gt; | &lt;/span&gt;&lt;span&gt;grep&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;-i&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;ipv6&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;2&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;networksetup&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;-setv6automatic&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;Wi-Fi&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;/code&gt;&lt;/pre&gt;&lt;div&gt;&lt;div&gt;&lt;/div&gt;&lt;div&gt;&lt;/div&gt;&lt;/div&gt;&lt;/figure&gt;&lt;/div&gt;&lt;p&gt;第一条显示 &lt;code&gt;IPv6: Link-Local Only&lt;/code&gt; 就是这个问题，第二条把它改回「自动」（也可以在「系统设置 → 网络 → Wi-Fi → 详细信息 → TCP/IP」里把「配置 IPv6」改成「自动」）。改回之后国内网站立刻恢复，配置里那两处全局的 IPv6 限制也就去掉了。sing-box 的虚拟网卡本身会接管 IPv6 流量，不存在 Clash 时期担心的「走 IPv6 绕过分流」。&lt;/p&gt;&lt;p&gt;&lt;strong&gt;去掉 IPv6 限制后，Claude 桌面版和电脑断开了连接。&lt;/strong&gt; 全局限制去掉之后，国内网站正常了，在 Claude 网页上聊天也正常，但 Claude 桌面版连接这台电脑的功能（让 Claude 读写电脑上的文件）一直显示断开，重启桌面版也没用。&lt;/p&gt;&lt;p&gt;原因还是 IPv6，只不过这次出在 eSIM 那一侧：限制全部去掉后，Claude 的域名在 DNS 里也会返回 IPv6 地址，而这些连接走 &lt;code&gt;esim&lt;/code&gt; 出口，eSIM 漫游只有 IPv4，IPv6 连不通。网页聊天遇到 IPv6 连不上会自己退回 IPv4，所以看不出问题；桌面版那条长连接就没那么「聪明」，卡在 IPv6 上一直连不起来。&lt;/p&gt;&lt;p&gt;修法是把 IPv6 限制收窄到真正需要的地方：&lt;strong&gt;只对要走 eSIM 的域名不返回 IPv6 地址&lt;/strong&gt;，也就是配置里 DNS 规则的第一条。改完重启 sing-box 和桌面版，连接马上恢复。这其实就是 iPhone 最早那版配置的写法，中间为了解决国内网站的问题把它扩大成了全局限制，再全部去掉时又把这条必要的也一起删了。&lt;/p&gt;&lt;p&gt;总结一下 IPv6 的处理原则：&lt;strong&gt;哪条线路没有 IPv6，就只对走那条线路的域名拦掉 IPv6&lt;/strong&gt;。家里 Wi-Fi 有 IPv6，国内网站就放开；eSIM 没有，走 eSIM 的域名就只给 IPv4。&lt;/p&gt;&lt;p&gt;Chrome 的「安全 DNS」也会绕过系统 DNS。它默认是「自动」模式，只有系统 DNS 正好是 Google、Cloudflare 这类知名服务商时才会升级成加密 DNS；系统 DNS 是家里路由器时不会触发。所以只要没手动选过某个服务商，保持默认就行，可以在 Chrome 地址栏输入 &lt;code&gt;chrome://settings/security&lt;/code&gt; 检查。就算触发了，分流规则里的 &lt;code&gt;sniff&lt;/code&gt; 也能从握手里读出域名。&lt;/p&gt;&lt;p&gt;&lt;strong&gt;查不出是哪个 App 发的连接。&lt;/strong&gt; 日志里还有几个只有 IP、走 Wi-Fi、5 秒超时的连接，想知道是哪个 App 发的。sing-box 有个 &lt;code&gt;find_process&lt;/code&gt; 选项能记录进程名，但 Mac 上的独立版运行在系统沙盒里，日志只给出 &lt;code&gt;failed to search process: Couldn’t communicate with a helper application&lt;/code&gt;，查不了。这种情况可以用系统自带的 &lt;code&gt;lsof&lt;/code&gt; 查，比如：&lt;/p&gt;&lt;div&gt;&lt;figure&gt;&lt;figcaption&gt;&lt;span&gt;&lt;/span&gt;&lt;span&gt;Terminal window&lt;/span&gt;&lt;/figcaption&gt;&lt;pre&gt;&lt;code&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;1&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;sudo&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;lsof&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;-nP&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;-i&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;@173.255.213.90&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;/code&gt;&lt;/pre&gt;&lt;div&gt;&lt;div&gt;&lt;/div&gt;&lt;div&gt;&lt;/div&gt;&lt;/div&gt;&lt;/figure&gt;&lt;/div&gt;&lt;/section&gt;&lt;section&gt;&lt;h3&gt;偷跑 eSIM 流量的漏洞&lt;a href=&quot;#偷跑-esim-流量的漏洞&quot;&gt;&lt;span&gt;#&lt;/span&gt;&lt;/a&gt;&lt;/h3&gt;&lt;p&gt;&lt;strong&gt;iPhone 上，国内网站可能被 eSIM 抢先连上。&lt;/strong&gt; 一开始 iPhone 的 Wi-Fi 出口设的是「Wi-Fi 优先，失败换蜂窝」（&lt;code&gt;network_strategy: fallback&lt;/code&gt;），想着出门时开着 sing-box 也不会断网。后来回头细看官方文档才发现，这个模式的行为是：Wi-Fi 上的连接只要 &lt;strong&gt;300 毫秒内没建立好&lt;/strong&gt;，就会&lt;strong&gt;同时&lt;/strong&gt;用蜂窝再发起一个连接，谁先连上用谁。国内稍慢一点的服务器，都可能被 eSIM 抢先连上，完全察觉不到。&lt;/p&gt;&lt;p&gt;改成普通直连、跟随系统默认网络之后就没这个问题了：在家走 Wi-Fi，出门走蜂窝，不会同时发起两个连接。&lt;/p&gt;&lt;p&gt;&lt;strong&gt;Mac 上，热点探测太频繁。&lt;/strong&gt; 这是 Clash 时期的问题：「热点优先」组每次探测都要经热点建立一次加密连接，我一度把间隔设成 30 秒，一天两千多次，一个月可能要几百 MB 热点流量。sing-box 方案没有探测，也就没有这个问题。&lt;/p&gt;&lt;p&gt;&lt;strong&gt;规则集天天下载。&lt;/strong&gt; 规则列表加起来有几 MB，原来每天更新，一个月也有几十 MB 走 eSIM。这些列表变化很慢，现在都改成每 7 天更新一次。&lt;/p&gt;&lt;p&gt;&lt;strong&gt;iPhone 的「无线局域网助理」和后台任务。&lt;/strong&gt; 前者在 Wi-Fi 信号差时会自动改用蜂窝，后者（照片同步、自动更新）在没有 Wi-Fi 时会用蜂窝。前者关掉，后者靠 eSIM 的「低数据模式」压住，见第七节。&lt;/p&gt;&lt;/section&gt;&lt;section&gt;&lt;h3&gt;sing-box 的弃用警告&lt;a href=&quot;#sing-box-的弃用警告&quot;&gt;&lt;span&gt;#&lt;/span&gt;&lt;/a&gt;&lt;/h3&gt;&lt;p&gt;第一次导入时弹出「弃用警告」：远程规则集的 &lt;code&gt;download_detour&lt;/code&gt; 写法在 1.14 已被弃用，1.16 会移除。这不是报错，配置照常能运行。既然装的已经是 1.14 以上，就改成了新写法：定义一个经 &lt;code&gt;esim&lt;/code&gt; 出口下载的 HTTP 客户端（&lt;code&gt;http_clients&lt;/code&gt;），在 &lt;code&gt;route.default_http_client&lt;/code&gt; 里设为默认。&lt;/p&gt;&lt;/section&gt;&lt;/section&gt;
&lt;section&gt;&lt;h2&gt;十、省流量：这些服务也会走 eSIM&lt;a href=&quot;#十省流量这些服务也会走-esim&quot;&gt;&lt;span&gt;#&lt;/span&gt;&lt;/a&gt;&lt;/h2&gt;&lt;p&gt;按规则走 eSIM 的网站里，有几类特别费流量，要心里有数：&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;YouTube&lt;/td&gt;&lt;td&gt;在 Google 列表里，视频全走 eSIM&lt;/td&gt;&lt;td&gt;少看，或调低画质&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;Homebrew 安装软件&lt;/td&gt;&lt;td&gt;很多安装包和源码从 GitHub 下载&lt;/td&gt;&lt;td&gt;配国内镜像；Intel + Sonoma 的 Mac 没有编译好的安装包，要从各软件官网下载源码编译，镜像只能加速一部分&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;Docker 拉镜像&lt;/td&gt;&lt;td&gt;Docker Hub 在境外网站列表里&lt;/td&gt;&lt;td&gt;配置国内镜像源&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;Hugging Face 下载模型&lt;/td&gt;&lt;td&gt;在境外网站列表里&lt;/td&gt;&lt;td&gt;模型文件动辄几个 GB，尽量别在 eSIM 上下载&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;Chrome 自动更新&lt;/td&gt;&lt;td&gt;从 Google 服务器下载&lt;/td&gt;&lt;td&gt;留意一下就行，一次一两百 MB&lt;/td&gt;&lt;/tr&gt;&lt;/tbody&gt;&lt;/table&gt;&lt;p&gt;npm 用的是国内镜像（&lt;code&gt;registry.npmmirror.com&lt;/code&gt;），不受影响。&lt;/p&gt;&lt;/section&gt;
&lt;section&gt;&lt;h2&gt;十一、怎么确认分流生效了&lt;a href=&quot;#十一怎么确认分流生效了&quot;&gt;&lt;span&gt;#&lt;/span&gt;&lt;/a&gt;&lt;/h2&gt;&lt;p&gt;&lt;strong&gt;看 sing-box 的「连接」页面。&lt;/strong&gt; Mac 和 iPhone 上都有。打开几个网站：&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;google.com、github.com、claude.ai、chatgpt.com&lt;/td&gt;&lt;td&gt;&lt;code&gt;esim&lt;/code&gt;&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;baidu.com、bilibili.com&lt;/td&gt;&lt;td&gt;&lt;code&gt;default&lt;/code&gt;&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;Steam 下载游戏&lt;/td&gt;&lt;td&gt;&lt;code&gt;default&lt;/code&gt;&lt;/td&gt;&lt;/tr&gt;&lt;/tbody&gt;&lt;/table&gt;&lt;p&gt;&lt;strong&gt;看日志。&lt;/strong&gt; Mac 上走 eSIM 的连接会显示 &lt;code&gt;outbound/direct[esim]: dial en6&lt;/code&gt; 这样的记录，&lt;code&gt;en6&lt;/code&gt; 是 iPhone 热点网卡，说明 &lt;code&gt;ethernet&lt;/code&gt; 那一招生效了。&lt;/p&gt;&lt;p&gt;&lt;strong&gt;留意这种连接：&lt;/strong&gt; 只显示 IP、走的是 &lt;code&gt;default&lt;/code&gt;、5 秒左右超时。这基本就是一个本该走 eSIM 却没被规则认出来的连接，可以用第九节的 &lt;code&gt;lsof&lt;/code&gt; 查是哪个 App。&lt;/p&gt;&lt;p&gt;&lt;strong&gt;看 eSIM 用了多少流量。&lt;/strong&gt; 最准的是 iPhone 上的「设置 → 蜂窝网络」：eSIM 线路的总用量里，Mac 用掉的部分单独记在「个人热点」那一项。可以每月初点一次最底下的「重置统计数据」。Mac 上也能看这次插线以来的热点用量：&lt;/p&gt;&lt;div&gt;&lt;figure&gt;&lt;figcaption&gt;&lt;span&gt;&lt;/span&gt;&lt;span&gt;Terminal window&lt;/span&gt;&lt;/figcaption&gt;&lt;pre&gt;&lt;code&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;1&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;netstat&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;-ib&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;-I&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;en6&lt;/span&gt;&lt;span&gt; | &lt;/span&gt;&lt;span&gt;awk&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;&apos;NR==2{printf &quot;下载 %.1f MB，上传 %.1f MB\n&quot;, $7/1048576, $10/1048576}&apos;&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;/code&gt;&lt;/pre&gt;&lt;div&gt;&lt;div&gt;&lt;/div&gt;&lt;div&gt;&lt;/div&gt;&lt;/div&gt;&lt;/figure&gt;&lt;/div&gt;&lt;p&gt;把 &lt;code&gt;en6&lt;/code&gt; 换成你的热点网卡名。&lt;/p&gt;&lt;/section&gt;
&lt;section&gt;&lt;h2&gt;十二、规则从哪来，以后怎么加&lt;a href=&quot;#十二规则从哪来以后怎么加&quot;&gt;&lt;span&gt;#&lt;/span&gt;&lt;/a&gt;&lt;/h2&gt;&lt;section&gt;&lt;h3&gt;社区规则的来源&lt;a href=&quot;#社区规则的来源&quot;&gt;&lt;span&gt;#&lt;/span&gt;&lt;/a&gt;&lt;/h3&gt;&lt;p&gt;现在的规则全部来自两个开源项目：&lt;/p&gt;&lt;ul&gt;
&lt;li&gt;&lt;a href=&quot;https://github.com/v2fly/domain-list-community&quot; target=&quot;_blank&quot;&gt;v2fly/domain-list-community&lt;/a&gt;：&lt;strong&gt;源头&lt;/strong&gt;。社区手工维护的「服务 → 域名」清单，&lt;code&gt;data&lt;/code&gt; 目录里一个文件就是一个列表，纯文本，网页上就能看，比如 &lt;code&gt;data/anthropic&lt;/code&gt;、&lt;code&gt;data/openai&lt;/code&gt;、&lt;code&gt;data/xai&lt;/code&gt;。&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://github.com/MetaCubeX/meta-rules-dat&quot; target=&quot;_blank&quot;&gt;MetaCubeX/meta-rules-dat&lt;/a&gt;：每天自动从上面编译成规则集。&lt;code&gt;sing&lt;/code&gt; 分支给 sing-box（&lt;code&gt;.srs&lt;/code&gt;），&lt;code&gt;meta&lt;/code&gt; 分支给 Clash（&lt;code&gt;.mrs&lt;/code&gt;）；&lt;code&gt;meta&lt;/code&gt; 分支里还有同名的 &lt;code&gt;.list&lt;/code&gt; 文本版，想看内容可以直接打开。&lt;/li&gt;
&lt;/ul&gt;&lt;p&gt;列表分两种：&lt;code&gt;geosite&lt;/code&gt; 按&lt;strong&gt;域名&lt;/strong&gt;分组，是主力；&lt;code&gt;geoip&lt;/code&gt; 按 &lt;strong&gt;IP 段&lt;/strong&gt;分组，只用来兜底没有域名的连接。&lt;/p&gt;&lt;/section&gt;&lt;section&gt;&lt;h3&gt;加一个服务&lt;a href=&quot;#加一个服务&quot;&gt;&lt;span&gt;#&lt;/span&gt;&lt;/a&gt;&lt;/h3&gt;&lt;p&gt;遇到打不开的网站，先在 v2fly 的 &lt;code&gt;data&lt;/code&gt; 目录里找有没有现成的列表。有的话，在 &lt;code&gt;route.rule_set&lt;/code&gt; 里加一个规则集，订阅地址把文件名换成列表名：&lt;/p&gt;&lt;div&gt;&lt;figure&gt;&lt;figcaption&gt;&lt;/figcaption&gt;&lt;pre&gt;&lt;code&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;1&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;https://raw.githubusercontent.com/MetaCubeX/meta-rules-dat/sing/geo/geosite/列表名.srs&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;/code&gt;&lt;/pre&gt;&lt;div&gt;&lt;div&gt;&lt;/div&gt;&lt;div&gt;&lt;/div&gt;&lt;/div&gt;&lt;/figure&gt;&lt;/div&gt;&lt;p&gt;再把它的 &lt;code&gt;tag&lt;/code&gt; 加进 DNS 规则和分流规则里那两个 &lt;code&gt;[&quot;anthropic&quot;, &quot;openai&quot;, ...]&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;code&gt;category-ai-!cn&lt;/code&gt;&lt;/td&gt;&lt;td&gt;境外 AI 服务合集，但里面有 Hugging Face、Ollama 这类下载大模型的网站，全走 eSIM 很费流量&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;&lt;code&gt;geolocation-!cn&lt;/code&gt;&lt;/td&gt;&lt;td&gt;「所有境外网站」，大量本来能直连的流量也会走 eSIM&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;&lt;code&gt;xai&lt;/code&gt;、&lt;code&gt;telegram&lt;/code&gt;、&lt;code&gt;twitter&lt;/code&gt; 等&lt;/td&gt;&lt;td&gt;主要域名已经在境外网站列表里，加了只是重复&lt;/td&gt;&lt;/tr&gt;&lt;/tbody&gt;&lt;/table&gt;&lt;p&gt;如果是社区列表漏了某个域名，可以去 v2fly 提 Issue 或 PR，合并后所有人都能用上。&lt;/p&gt;&lt;/section&gt;&lt;section&gt;&lt;h3&gt;临时加一个域名&lt;a href=&quot;#临时加一个域名&quot;&gt;&lt;span&gt;#&lt;/span&gt;&lt;/a&gt;&lt;/h3&gt;&lt;p&gt;社区列表还没收录时，可以在配置里临时加两处：&lt;/p&gt;&lt;ol&gt;
&lt;li&gt;
&lt;p&gt;&lt;code&gt;dns.rules&lt;/code&gt; 里 &lt;code&gt;&quot;server&quot;: &quot;dns-esim&quot;&lt;/code&gt; 那条前面，加一条让它用境外 DNS 解析：&lt;/p&gt;
&lt;div&gt;&lt;figure&gt;&lt;figcaption&gt;&lt;/figcaption&gt;&lt;pre&gt;&lt;code&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;1&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;{ &lt;/span&gt;&lt;span&gt;&quot;domain_suffix&quot;&lt;/span&gt;&lt;span&gt;: [&lt;/span&gt;&lt;span&gt;&quot;example.com&quot;&lt;/span&gt;&lt;span&gt;], &lt;/span&gt;&lt;span&gt;&quot;action&quot;&lt;/span&gt;&lt;span&gt;: &lt;/span&gt;&lt;span&gt;&quot;route&quot;&lt;/span&gt;&lt;span&gt;, &lt;/span&gt;&lt;span&gt;&quot;server&quot;&lt;/span&gt;&lt;span&gt;: &lt;/span&gt;&lt;span&gt;&quot;dns-esim&quot;&lt;/span&gt;&lt;span&gt; },&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;/code&gt;&lt;/pre&gt;&lt;div&gt;&lt;div&gt;&lt;/div&gt;&lt;div&gt;&lt;/div&gt;&lt;/div&gt;&lt;/figure&gt;&lt;/div&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;code&gt;route.rules&lt;/code&gt; 里第一条 &lt;code&gt;&quot;outbound&quot;: &quot;esim&quot;&lt;/code&gt; 前面，加一条让它走 eSIM：&lt;/p&gt;
&lt;div&gt;&lt;figure&gt;&lt;figcaption&gt;&lt;/figcaption&gt;&lt;pre&gt;&lt;code&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;1&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;{ &lt;/span&gt;&lt;span&gt;&quot;domain_suffix&quot;&lt;/span&gt;&lt;span&gt;: [&lt;/span&gt;&lt;span&gt;&quot;example.com&quot;&lt;/span&gt;&lt;span&gt;], &lt;/span&gt;&lt;span&gt;&quot;action&quot;&lt;/span&gt;&lt;span&gt;: &lt;/span&gt;&lt;span&gt;&quot;route&quot;&lt;/span&gt;&lt;span&gt;, &lt;/span&gt;&lt;span&gt;&quot;outbound&quot;&lt;/span&gt;&lt;span&gt;: &lt;/span&gt;&lt;span&gt;&quot;esim&quot;&lt;/span&gt;&lt;span&gt; },&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;/code&gt;&lt;/pre&gt;&lt;div&gt;&lt;div&gt;&lt;/div&gt;&lt;div&gt;&lt;/div&gt;&lt;/div&gt;&lt;/figure&gt;&lt;/div&gt;
&lt;/li&gt;
&lt;/ol&gt;&lt;p&gt;改的是 iCloud 里的那个文件，两台设备都会生效，记得重启 sing-box。&lt;/p&gt;&lt;/section&gt;&lt;section&gt;&lt;h3&gt;不知道该加哪个域名时&lt;a href=&quot;#不知道该加哪个域名时&quot;&gt;&lt;span&gt;#&lt;/span&gt;&lt;/a&gt;&lt;/h3&gt;&lt;p&gt;打开那个网站或应用，再到 sing-box 的「连接」页面，看它实际访问了哪些域名、走的是哪个出口，把走错的那个加进去即可。&lt;/p&gt;&lt;/section&gt;&lt;/section&gt;
&lt;section&gt;&lt;h2&gt;十三、这套配置能多久不用动&lt;a href=&quot;#十三这套配置能多久不用动&quot;&gt;&lt;span&gt;#&lt;/span&gt;&lt;/a&gt;&lt;/h2&gt;

&lt;table&gt;&lt;thead&gt;&lt;tr&gt;&lt;th&gt;什么情况要改&lt;/th&gt;&lt;th&gt;频率&lt;/th&gt;&lt;th&gt;怎么处理&lt;/th&gt;&lt;/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;/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;出现弃用警告时再改&lt;/td&gt;&lt;/tr&gt;&lt;/tbody&gt;&lt;/table&gt;&lt;p&gt;最后一种最可能让配置失效，sing-box 几乎每个大版本都会淘汰一些旧写法。所以我关掉了 App Store 的自动更新（「设置 → App Store → 自动下载」），Mac 上的 Homebrew 版也不会自动更新，升级时机自己掌握。按现在的写法，至少要到 sing-box 1.16 才会有必须改的地方。&lt;/p&gt;&lt;/section&gt;
&lt;section&gt;&lt;h2&gt;十四、新设备快速配置清单&lt;a href=&quot;#十四新设备快速配置清单&quot;&gt;&lt;span&gt;#&lt;/span&gt;&lt;/a&gt;&lt;/h2&gt;&lt;section&gt;&lt;h3&gt;Mac&lt;a href=&quot;#mac&quot;&gt;&lt;span&gt;#&lt;/span&gt;&lt;/a&gt;&lt;/h3&gt;&lt;ol&gt;
&lt;li&gt;&lt;strong&gt;装 sing-box&lt;/strong&gt;：&lt;code&gt;brew install --cask sfm&lt;/code&gt;，第一次启动时允许网络扩展和 VPN 配置。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;连好网络&lt;/strong&gt;：
&lt;ul&gt;
&lt;li&gt;Wi-Fi 连家里路由器，关掉 iPhone 热点的「自动加入」&lt;/li&gt;
&lt;li&gt;iPhone 数据卡设成 eSIM，用数据线连电脑，打开「个人热点 → 允许其他人加入」，第一次连接时在手机上点「信任」&lt;/li&gt;
&lt;li&gt;「系统设置 → 网络 → iPhone USB → 详细信息」，关掉「需要时启用」&lt;/li&gt;
&lt;li&gt;「设定服务顺序」里 Wi-Fi 排在 iPhone USB 上面&lt;/li&gt;
&lt;li&gt;Wi-Fi 的「配置 IPv6」是「自动」，不是「仅本地链接」&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;导入配置&lt;/strong&gt;：把第五节的配置保存为「iCloud 云盘 → sing-box」里的 &lt;code&gt;esim-split.json&lt;/code&gt;，在 sing-box 里新建 iCloud 配置并启动。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;验证&lt;/strong&gt;：打开 google.com 和 baidu.com，在「连接」页面看到 &lt;code&gt;esim&lt;/code&gt; 和 &lt;code&gt;default&lt;/code&gt;。&lt;/li&gt;
&lt;/ol&gt;&lt;/section&gt;&lt;section&gt;&lt;h3&gt;iPhone&lt;a href=&quot;#iphone&quot;&gt;&lt;span&gt;#&lt;/span&gt;&lt;/a&gt;&lt;/h3&gt;&lt;ol&gt;
&lt;li&gt;&lt;strong&gt;装 sing-box&lt;/strong&gt;：用外区 Apple ID 安装官方文档链接的 sing-box MT。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;导入配置&lt;/strong&gt;：新建 iCloud 配置，选同一个 &lt;code&gt;esim-split.json&lt;/code&gt;，打开开关。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;系统设置&lt;/strong&gt;：允许 sing-box 使用蜂窝数据；在家时数据卡设成 eSIM，关掉「允许切换蜂窝数据」；关掉「无线局域网助理」；给 eSIM 打开「低数据模式」。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;自动化&lt;/strong&gt;：按第七节建「连上家里 Wi-Fi 时切到 eSIM 并连接 sing-box」的快捷指令自动化。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;验证&lt;/strong&gt;：连着家里 Wi-Fi，分别打开 google.com 和 baidu.com，在「连接」页面看到 &lt;code&gt;esim&lt;/code&gt; 和 &lt;code&gt;default&lt;/code&gt;。&lt;/li&gt;
&lt;/ol&gt;&lt;blockquote&gt;&lt;p&gt;参考：&lt;a href=&quot;https://sing-box.sagernet.org/&quot; target=&quot;_blank&quot;&gt;sing-box 文档&lt;/a&gt;、&lt;a href=&quot;https://sing-box.sagernet.org/configuration/shared/dial/&quot; target=&quot;_blank&quot;&gt;sing-box 拨号字段（network_type）&lt;/a&gt;、&lt;a href=&quot;https://sing-box.sagernet.org/zh/clients/apple/&quot; target=&quot;_blank&quot;&gt;sing-box 苹果客户端&lt;/a&gt;、&lt;a href=&quot;https://formulae.brew.sh/cask/sfm&quot; target=&quot;_blank&quot;&gt;Homebrew：sfm&lt;/a&gt;、&lt;a href=&quot;https://github.com/v2fly/domain-list-community&quot; target=&quot;_blank&quot;&gt;v2fly/domain-list-community&lt;/a&gt;、&lt;a href=&quot;https://github.com/MetaCubeX/meta-rules-dat&quot; target=&quot;_blank&quot;&gt;MetaCubeX/meta-rules-dat&lt;/a&gt;、&lt;a href=&quot;https://wiki.metacubex.one/&quot; target=&quot;_blank&quot;&gt;mihomo 文档&lt;/a&gt;、&lt;a href=&quot;https://support.apple.com/109317&quot; target=&quot;_blank&quot;&gt;苹果：配合 eSIM 使用双卡&lt;/a&gt;。&lt;/p&gt;&lt;/blockquote&gt;&lt;/section&gt;&lt;/section&gt;</content:encoded></item><item><title>给 2016 款 MacBook Pro 装上 macOS Sonoma：OpenCore Legacy Patcher 实录</title><link>https://tracerzzz.cn/posts/oclp-sonoma-2016-macbook-pro/</link><guid isPermaLink="true">https://tracerzzz.cn/posts/oclp-sonoma-2016-macbook-pro/</guid><description>家里有一台 2016 款 15 英寸 MacBook Pro，官方最高只能升到 macOS 12，装不了要求 macOS 13 以上的新软件。用 OpenCore Legacy Patcher 把它升到了 Sonoma 14，这篇记录选版本的考虑、安装步骤，以及装完后遇到的屏幕闪烁、Wi-Fi 消失、指纹失效、iCloud 退不出去、睡眠唤醒死机这些坑。</description><pubDate>Mon, 26 Oct 2026 17:30:00 GMT</pubDate><content:encoded>&lt;blockquote&gt;&lt;p&gt;2026-10-27 晚更新：用了一天后发现一睡眠就再也唤醒不了，补上了&lt;a href=&quot;#%E7%9D%A1%E7%9C%A0%E5%90%8E%E5%94%A4%E9%86%92%E5%B0%B1%E6%AD%BB%E6%9C%BA&quot;&gt;睡眠唤醒死机&lt;/a&gt;的排查过程和最终设置。&lt;/p&gt;&lt;/blockquote&gt;
&lt;p&gt;家里有一台 2016 款 15 英寸 MacBook Pro（i7 四核、16GB 内存），一直停在 macOS 12 Monterey。这是苹果给它的最后一个版本，“软件更新”里不会再出现新系统。&lt;/p&gt;
&lt;p&gt;平时用着没什么问题，直到想装一个桌面应用，发现它要求 macOS 13 以上。越来越多的新软件都是这个要求，这台机器眼看就要被软件淘汰了。于是用 &lt;strong&gt;OpenCore Legacy Patcher&lt;/strong&gt;（简称 OCLP）把它升到了 macOS 14 Sonoma。&lt;/p&gt;
&lt;p&gt;这篇记录整个过程，包括中间踩到的坑。&lt;/p&gt;
&lt;section&gt;&lt;h2&gt;一、OCLP 是什么&lt;a href=&quot;#一oclp-是什么&quot;&gt;&lt;span&gt;#&lt;/span&gt;&lt;/a&gt;&lt;/h2&gt;&lt;p&gt;OCLP 是社区维护的开源工具，专门让苹果已经不支持的老 Mac 装上新系统。它做两件事：&lt;/p&gt;&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;开机引导&lt;/strong&gt;：在硬盘的 EFI 分区装一个 OpenCore 引导器。每次开机先经过它，它让新系统”认为”这台机器是受支持的型号。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;补驱动&lt;/strong&gt;：新系统删掉了老硬件的驱动（显卡、无线网卡等），OCLP 装完系统后再把这些驱动补回去，叫 &lt;strong&gt;Root Patch&lt;/strong&gt;。&lt;/li&gt;
&lt;/ul&gt;&lt;p&gt;这台 2016 款 15 英寸（型号 &lt;code&gt;MacBookPro13,3&lt;/code&gt;）在 OCLP 的支持列表里。&lt;/p&gt;&lt;/section&gt;
&lt;section&gt;&lt;h2&gt;二、选哪个版本&lt;a href=&quot;#二选哪个版本&quot;&gt;&lt;span&gt;#&lt;/span&gt;&lt;/a&gt;&lt;/h2&gt;&lt;p&gt;&lt;strong&gt;OCLP 版本：2.5.1。&lt;/strong&gt;下载时 Releases 页面最新的已经是 3.x，但都标着 Pre-release（预发布）。3.x 主要是为了支持 macOS 26 Tahoe，还在完善中，所以选了 2.x 里最新的正式版 2.5.1。&lt;/p&gt;&lt;p&gt;注意：从 2.3.0 开始，官方不再提供 &lt;code&gt;OpenCore-Patcher-GUI.app.zip&lt;/code&gt;，要下载的是 &lt;strong&gt;&lt;code&gt;OpenCore-Patcher.pkg&lt;/code&gt;&lt;/strong&gt;，双击安装后在”应用程序”里打开。&lt;/p&gt;&lt;p&gt;&lt;strong&gt;macOS 版本：14 Sonoma。&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;13 Ventura&lt;/td&gt;&lt;td&gt;能满足”13 以上”的要求，但没有理由不再往上一版&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;&lt;strong&gt;14 Sonoma&lt;/strong&gt;&lt;/td&gt;&lt;td&gt;在老机型上用 OCLP 安装的人多，补丁成熟，选它&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;15 Sequoia&lt;/td&gt;&lt;td&gt;OCLP 2.x 的发布说明里一直写着”支持仍在积极开发中”；它的亮点功能（iPhone 镜像、Apple 智能）在 Intel 机器上本来就用不了&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;26 Tahoe&lt;/td&gt;&lt;td&gt;只有 3.x 预发布版支持，而且是最后一个支持 Intel 的系统，不折腾&lt;/td&gt;&lt;/tr&gt;&lt;/tbody&gt;&lt;/table&gt;&lt;/section&gt;
&lt;section&gt;&lt;h2&gt;三、安装前要想清楚的两件事&lt;a href=&quot;#三安装前要想清楚的两件事&quot;&gt;&lt;span&gt;#&lt;/span&gt;&lt;/a&gt;&lt;/h2&gt;&lt;section&gt;&lt;h3&gt;要不要备份&lt;a href=&quot;#要不要备份&quot;&gt;&lt;span&gt;#&lt;/span&gt;&lt;/a&gt;&lt;/h3&gt;&lt;p&gt;稳妥的做法是先用时间机器备份一次。备份防两种意外：安装中途失败系统起不来；装完不满意想退回去。后一种最麻烦，从新系统退回旧系统必须抹盘重装。&lt;/p&gt;&lt;p&gt;我这台机器上没有什么要紧的东西，就没有备份。如果你的电脑里有重要文件，一定先备份。&lt;/p&gt;&lt;/section&gt;&lt;section&gt;&lt;h3&gt;要不要抹盘重装&lt;a href=&quot;#要不要抹盘重装&quot;&gt;&lt;span&gt;#&lt;/span&gt;&lt;/a&gt;&lt;/h3&gt;&lt;p&gt;&lt;strong&gt;不要抹盘&lt;/strong&gt;，直接在原系统上升级。原因在 T1 芯片。&lt;/p&gt;&lt;p&gt;2016、2017 款带 Touch Bar 的 MacBook Pro 都有一颗 &lt;strong&gt;T1 芯片&lt;/strong&gt;，它是苹果的第一代安全芯片，相当于电脑里的一台小电脑，负责 Touch Bar、Touch ID 指纹、Apple Pay。&lt;/p&gt;&lt;p&gt;Sonoma 删掉了对 T1 的支持。OCLP 1.1.0 以后能补回来，但前提是硬盘上还留着旧系统里和 T1 配合用的文件。OCLP 官方文档写明：&lt;strong&gt;抹掉硬盘会清除 T1 的固件，并且不会重新安装。&lt;/strong&gt;所以直接升级更稳。&lt;/p&gt;&lt;p&gt;想要一台干净的系统，正确顺序是：先在恢复模式里抹盘、重装一个全新的 Monterey，再用 OCLP 升级到 Sonoma。&lt;/p&gt;&lt;/section&gt;&lt;/section&gt;
&lt;section&gt;&lt;h2&gt;四、安装步骤&lt;a href=&quot;#四安装步骤&quot;&gt;&lt;span&gt;#&lt;/span&gt;&lt;/a&gt;&lt;/h2&gt;&lt;p&gt;需要准备一块 16GB 以上的 U 盘或移动硬盘。我用的是一块 1TB 的希捷移动硬盘，原来是 NTFS 格式，Mac 上只能读不能写。这不影响做安装盘：OCLP 写入时会把整块盘抹掉，重新分区成 macOS 的格式，根本不会往 NTFS 里写东西。&lt;/p&gt;&lt;ol&gt;
&lt;li&gt;&lt;strong&gt;安装 OCLP&lt;/strong&gt;：下载 &lt;code&gt;OpenCore-Patcher.pkg&lt;/code&gt;，双击安装，打开 OpenCore-Patcher。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;做安装盘&lt;/strong&gt;：点 &lt;strong&gt;Create macOS Installer → Download macOS Installer&lt;/strong&gt;，选 Sonoma 14.x 的最新版，下载约 13GB，然后写入移动硬盘。选盘时看清容量，别选成内置硬盘。
&lt;ul&gt;
&lt;li&gt;安装器走的是苹果官方的软件更新服务器，国内下载速度还可以；&lt;/li&gt;
&lt;li&gt;写入时进度条会在某个百分比停很久，这是正常的。可以打开”活动监视器”的”磁盘”标签页，看写入的数据还在不在涨；&lt;/li&gt;
&lt;li&gt;已经有”安装 macOS Sonoma.app”的话，可以选 &lt;strong&gt;Use Existing macOS Installer&lt;/strong&gt;，省掉下载。&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;把 OpenCore 装到内置硬盘&lt;/strong&gt;：点 &lt;strong&gt;Build and Install OpenCore → Install to disk&lt;/strong&gt;，选内置硬盘的 EFI 分区。装在内置硬盘上，以后开机就自动经过 OpenCore，不用插着 U 盘。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;从安装盘启动&lt;/strong&gt;：插着移动硬盘重启，按住 &lt;strong&gt;Option&lt;/strong&gt; 键，选 &lt;strong&gt;EFI Boot&lt;/strong&gt;，再在 OpenCore 的菜单里选 &lt;strong&gt;Install macOS Sonoma&lt;/strong&gt;。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;安装&lt;/strong&gt;：目标选 &lt;strong&gt;Macintosh HD&lt;/strong&gt;，&lt;strong&gt;不要进磁盘工具抹盘&lt;/strong&gt;，直接安装就是保留数据的升级。中途会自动重启好几次，黑屏、白苹果卡很久都正常，全程约一小时。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;打补丁&lt;/strong&gt;：进入系统后打开 OCLP，点 &lt;strong&gt;Post-Install Root Patch → Start Root Patching&lt;/strong&gt;，完成后重启。&lt;/li&gt;
&lt;/ol&gt;&lt;/section&gt;
&lt;section&gt;&lt;h2&gt;五、踩到的坑&lt;a href=&quot;#五踩到的坑&quot;&gt;&lt;span&gt;#&lt;/span&gt;&lt;/a&gt;&lt;/h2&gt;&lt;section&gt;&lt;h3&gt;选了 EFI Boot，还没来得及选就直接进了旧系统&lt;a href=&quot;#选了-efi-boot还没来得及选就直接进了旧系统&quot;&gt;&lt;span&gt;#&lt;/span&gt;&lt;/a&gt;&lt;/h3&gt;&lt;p&gt;OpenCore 的选择菜单默认隐藏或者倒计时很短，选了 EFI Boot 之后，它自动启动了默认系统，也就是原来的 Monterey。&lt;/p&gt;&lt;p&gt;解决：选 EFI Boot 回车后，&lt;strong&gt;立刻按住 Option 键（或 Esc）&lt;/strong&gt;，直到出现 OpenCore 的菜单。也可以在 OCLP 的 &lt;strong&gt;Settings → Build&lt;/strong&gt; 里打开 &lt;strong&gt;Show OpenCore Bootpicker&lt;/strong&gt;，重新 Build and Install OpenCore，以后每次开机都会显示菜单。&lt;/p&gt;&lt;/section&gt;&lt;section&gt;&lt;h3&gt;装完屏幕一直闪，设置向导里搜不到 Wi-Fi&lt;a href=&quot;#装完屏幕一直闪设置向导里搜不到-wi-fi&quot;&gt;&lt;span&gt;#&lt;/span&gt;&lt;/a&gt;&lt;/h3&gt;&lt;p&gt;这两个是同一个原因：Sonoma 里没有这台机器集成显卡和无线网卡的驱动，要靠 Root Patch 补回来。&lt;/p&gt;&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;屏幕闪&lt;/strong&gt;：Intel 集成显卡没有驱动。可以先在”系统设置 → 电池 → 选项”里关掉”自动切换显卡”，让系统固定用 AMD 独立显卡（它的驱动 Sonoma 自带），屏幕就不闪了。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;没有 Wi-Fi&lt;/strong&gt;：设置向导里点”其他网络选项 → 我的电脑未连接到互联网”跳过，Apple ID 也选”稍后设置”，先进桌面。&lt;/li&gt;
&lt;/ul&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;Graphics: Intel Skylake&lt;/td&gt;&lt;td&gt;Intel HD 530 集成显卡驱动&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;Graphics: AMD Polaris&lt;/td&gt;&lt;td&gt;AMD Radeon Pro 独立显卡相关的补丁&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;Networking: Modern Wireless&lt;/td&gt;&lt;td&gt;博通无线网卡驱动&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;Miscellaneous: T1 Security Chip&lt;/td&gt;&lt;td&gt;T1 芯片支持&lt;/td&gt;&lt;/tr&gt;&lt;/tbody&gt;&lt;/table&gt;&lt;p&gt;补丁文件已经打包在 OCLP 的安装包里，我在没有联网的情况下打补丁也成功了。打完重启，屏幕不闪了，Wi-Fi 也回来了。日志里会提到 KDK（内核开发包），部分情况下要从 GitHub 下载它，如果打补丁时提示下载失败，可以用 iPhone 的个人热点通过数据线给 Mac 临时联网。&lt;/p&gt;&lt;/section&gt;&lt;section&gt;&lt;h3&gt;Touch ID 录不进指纹&lt;a href=&quot;#touch-id-录不进指纹&quot;&gt;&lt;span&gt;#&lt;/span&gt;&lt;/a&gt;&lt;/h3&gt;&lt;p&gt;升级后 Touch Bar 正常，说明 T1 芯片在工作，补丁也生效了；但在”触控 ID 与密码”里添加指纹，输完密码没有反应。&lt;/p&gt;&lt;p&gt;试过的办法：&lt;/p&gt;&lt;ul&gt;
&lt;li&gt;完整关机再开机，让 T1 重新初始化；&lt;/li&gt;
&lt;li&gt;在 OCLP 里 &lt;strong&gt;Revert Root Patches&lt;/strong&gt; 撤销补丁、重启，再重新打一遍补丁。&lt;/li&gt;
&lt;/ul&gt;&lt;p&gt;都没用。社区里有人通过修改 OCLP 的 SMBIOS 伪装设置恢复了 Touch ID，但也有人改完开不了机。这台是家庭备用机，用密码解锁完全够用，就没有再折腾。&lt;/p&gt;&lt;/section&gt;&lt;section&gt;&lt;h3&gt;iCloud 退不出登录，照片、音乐也打不开&lt;a href=&quot;#icloud-退不出登录照片音乐也打不开&quot;&gt;&lt;span&gt;#&lt;/span&gt;&lt;/a&gt;&lt;/h3&gt;&lt;p&gt;升级后想退出 iCloud，一直转圈。强制退出、重启、先关”查找我的 Mac”都不行，接着自带的照片、音乐也打不开了。&lt;/p&gt;&lt;p&gt;原因是退出过程卡在一半，账户状态乱了；照片和音乐启动时要读 iCloud 账户和钥匙串，也跟着卡住。&lt;/p&gt;&lt;p&gt;解决办法是&lt;strong&gt;新建一个管理员用户&lt;/strong&gt;，用新用户登录后一切正常。说明系统没问题，只是旧用户的账户数据坏了。把旧用户删掉，再在 iPhone 的”设置 → 你的名字”设备列表里把这台 Mac 从账户中移除，就彻底干净了。&lt;/p&gt;&lt;/section&gt;&lt;section&gt;&lt;h3&gt;睡眠后唤醒就死机&lt;a href=&quot;#睡眠后唤醒就死机&quot;&gt;&lt;span&gt;#&lt;/span&gt;&lt;/a&gt;&lt;/h3&gt;&lt;p&gt;装好用了一天，发现只要电脑进入睡眠，再唤醒就卡死：屏幕闪一下或者干脆不亮，键盘鼠标都没反应，只能长按电源键强制关机。&lt;/p&gt;&lt;p&gt;&lt;strong&gt;先看崩溃日志。&lt;/strong&gt; macOS 会把这类故障记在 &lt;code&gt;/Library/Logs/DiagnosticReports/&lt;/code&gt; 里，一共找到两种文件：&lt;/p&gt;&lt;ul&gt;
&lt;li&gt;&lt;code&gt;Kernel-*.panic&lt;/code&gt;：内核崩溃记录&lt;/li&gt;
&lt;li&gt;&lt;code&gt;Sleep Wake Failure_*.diag&lt;/code&gt;：睡眠唤醒失败记录&lt;/li&gt;
&lt;/ul&gt;&lt;p&gt;&lt;code&gt;.panic&lt;/code&gt; 文件有好几 MB，第一行是一段 JSON 元信息，第二行才是正文，崩溃原因在 &lt;code&gt;macOSPanicString&lt;/code&gt; 字段里，旁边还有一大段 base64 编码的现场快照。直接 &lt;code&gt;grep&lt;/code&gt; 搜不到东西，用 Python 解析一下就清楚了：&lt;/p&gt;&lt;div&gt;&lt;figure&gt;&lt;figcaption&gt;&lt;span&gt;&lt;/span&gt;&lt;span&gt;Terminal window&lt;/span&gt;&lt;/figcaption&gt;&lt;pre&gt;&lt;code&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;1&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;f&lt;/span&gt;&lt;span&gt;&lt;span&gt;=&lt;/span&gt;&lt;span&gt;$(&lt;/span&gt;&lt;/span&gt;&lt;span&gt;ls&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;-t&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;/Library/Logs/DiagnosticReports/Kernel-&lt;/span&gt;&lt;span&gt;*&lt;/span&gt;&lt;span&gt;.panic&lt;/span&gt;&lt;span&gt; | &lt;/span&gt;&lt;span&gt;head&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;-1&lt;/span&gt;&lt;span&gt;)&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;2&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;python3&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;-&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;&quot;&lt;/span&gt;&lt;span&gt;$f&lt;/span&gt;&lt;span&gt;&quot;&lt;/span&gt;&lt;span&gt; &amp;lt;&amp;lt;&lt;/span&gt;&lt;span&gt;&apos;PY&apos;&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;3&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;import json, sys&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;4&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;body = open(sys.argv[1], errors=&quot;ignore&quot;).read().split(&quot;\n&quot;, 1)[1]&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;5&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;d, _ = json.JSONDecoder().raw_decode(body)&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;6&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;print(d.get(&quot;macOSPanicString&quot;, &quot;&quot;)[:1500])&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;7&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;PY&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;/code&gt;&lt;/pre&gt;&lt;div&gt;&lt;div&gt;&lt;/div&gt;&lt;div&gt;&lt;/div&gt;&lt;/div&gt;&lt;/figure&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;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;AMDFramebufferVIB::setPowerState ... timed out after 45290 ms&lt;/code&gt;&lt;/td&gt;&lt;td&gt;唤醒时给 AMD 独显重新上电，45 秒没完成，系统判定超时崩溃&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;唤醒失败&lt;/td&gt;&lt;td&gt;卡在 &lt;code&gt;IOGraphicsSystem&lt;/code&gt;，等着 AMD Radeon 的驱动&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;Sleep Wake failure in EFI&lt;/code&gt;&lt;/td&gt;&lt;td&gt;只用核显时出现，在更早的固件阶段就失败了&lt;/td&gt;&lt;/tr&gt;&lt;/tbody&gt;&lt;/table&gt;&lt;p&gt;另外还有一次和睡眠无关的崩溃，出在 OCLP 补的无线网卡驱动（&lt;code&gt;AirPort.BrcmNIC&lt;/code&gt;）上，只出现过一次，先观察。&lt;/p&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;code&gt;sudo pmset -a standby 0&lt;/code&gt;）。有人在 2017 款 MacBook Pro 上用这招解决了 OCLP + Sonoma 的唤醒失败，两款硬件很接近，但在我这台上没用。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;只用独显&lt;/strong&gt;（&lt;code&gt;sudo pmset -a gpuswitch 1&lt;/code&gt;）。想看看是不是双显卡切换的问题，结果更糟：直接无法唤醒。这正好和日志对上了，问题就出在 AMD 独显的唤醒上。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;只用核显&lt;/strong&gt;（&lt;code&gt;sudo pmset -a gpuswitch 0&lt;/code&gt;），并关掉休眠镜像。躲开了独显，但唤醒在固件阶段又失败了，也就是上表的第三条。&lt;/li&gt;
&lt;/ol&gt;&lt;p&gt;15 寸 2016 款还有个硬件限制：雷雳接口是直接接在独显上的。我这台平时要接外接显示器，只要插上显示器，系统就会强制启用独显，所以「只用核显」这条路本来也走不通。&lt;/p&gt;&lt;p&gt;&lt;strong&gt;最终方案：不让系统睡眠，只关屏幕。&lt;/strong&gt; 既然唤醒这一步过不去，就干脆不睡：&lt;/p&gt;&lt;div&gt;&lt;figure&gt;&lt;figcaption&gt;&lt;span&gt;&lt;/span&gt;&lt;span&gt;Terminal window&lt;/span&gt;&lt;/figcaption&gt;&lt;pre&gt;&lt;code&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;1&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;sudo&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;pmset&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;-a&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;gpuswitch&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;2&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;2&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;sudo&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;pmset&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;-a&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;sleep&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;0&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;3&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;sudo&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;pmset&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;-a&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;disablesleep&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;1&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;4&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;sudo&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;pmset&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;-a&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;hibernatemode&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;0&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;5&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;sudo&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;pmset&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;-a&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;autopoweroff&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;0&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;6&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;sudo&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;pmset&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;-a&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;standby&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;0&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;7&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;sudo&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;pmset&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;-a&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;powernap&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;0&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;8&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;sudo&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;rm&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;-f&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;/var/vm/sleepimage&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;9&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;sudo&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;pmset&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;-a&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;displaysleep&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;10&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;/code&gt;&lt;/pre&gt;&lt;div&gt;&lt;div&gt;&lt;/div&gt;&lt;div&gt;&lt;/div&gt;&lt;/div&gt;&lt;/figure&gt;&lt;/div&gt;

&lt;table&gt;&lt;thead&gt;&lt;tr&gt;&lt;th&gt;设置&lt;/th&gt;&lt;th&gt;作用&lt;/th&gt;&lt;/tr&gt;&lt;/thead&gt;&lt;tbody&gt;&lt;tr&gt;&lt;td&gt;&lt;code&gt;gpuswitch 2&lt;/code&gt;&lt;/td&gt;&lt;td&gt;恢复显卡自动切换（接显示器时本来就会用独显）&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;&lt;code&gt;sleep 0&lt;/code&gt;、&lt;code&gt;disablesleep 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;hibernatemode 0&lt;/code&gt;、&lt;code&gt;autopoweroff 0&lt;/code&gt;、&lt;code&gt;standby 0&lt;/code&gt;&lt;/td&gt;&lt;td&gt;关掉休眠镜像和深度待机，不再往硬盘写内存镜像&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;&lt;code&gt;powernap 0&lt;/code&gt;&lt;/td&gt;&lt;td&gt;关掉睡眠时的后台唤醒&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;&lt;code&gt;displaysleep 10&lt;/code&gt;&lt;/td&gt;&lt;td&gt;10 分钟没操作就关屏幕&lt;/td&gt;&lt;/tr&gt;&lt;/tbody&gt;&lt;/table&gt;&lt;p&gt;&lt;strong&gt;熄屏要先测一下。&lt;/strong&gt; 熄屏后再点亮，也会让显卡的显示输出断电再上电，有可能触发同样的问题。我先把熄屏时间设成 1 分钟，等屏幕熄灭后动鼠标，能正常亮起，才改回 10 分钟。如果你的机器熄屏后也会卡死，就设成 &lt;code&gt;displaysleep 0&lt;/code&gt;（永不熄屏），改用显示器自己的节能功能。&lt;/p&gt;&lt;p&gt;改完用 &lt;code&gt;pmset -g&lt;/code&gt; 检查，应该能看到 &lt;code&gt;SleepDisabled 1&lt;/code&gt;、&lt;code&gt;sleep 0&lt;/code&gt;、&lt;code&gt;hibernatemode 0&lt;/code&gt; 这些值。&lt;/p&gt;&lt;p&gt;一个小坑：一开始我把带 &lt;code&gt;#&lt;/code&gt; 注释的命令直接粘进终端，结果好几行报 &lt;code&gt;Usage: pmset &amp;lt;options&amp;gt;&lt;/code&gt;。macOS 默认的 zsh 在交互模式下不把 &lt;code&gt;#&lt;/code&gt; 当注释，注释文字被当成参数传给了 pmset。不带注释重新执行就好了。&lt;/p&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;code&gt;Control + Command + Q&lt;/code&gt; 锁屏，锁屏不涉及睡眠。&lt;/li&gt;
&lt;li&gt;长时间不用就直接关机。&lt;/li&gt;
&lt;li&gt;以后升级了 OCLP，想再试试正常睡眠，把 &lt;code&gt;disablesleep&lt;/code&gt; 改回 0、&lt;code&gt;sleep&lt;/code&gt; 改回 1 即可。&lt;/li&gt;
&lt;/ul&gt;&lt;/section&gt;&lt;/section&gt;
&lt;section&gt;&lt;h2&gt;六、装完之后要知道的&lt;a href=&quot;#六装完之后要知道的&quot;&gt;&lt;span&gt;#&lt;/span&gt;&lt;/a&gt;&lt;/h2&gt;&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;别删 OCLP，别动 EFI 分区&lt;/strong&gt;：现在每次开机都要经过 OpenCore。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;少用”重置 NVRAM”&lt;/strong&gt;：重置后可能绕过 OpenCore，系统进不去。万一进不去，开机按住 Option 选 EFI Boot。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;系统更新&lt;/strong&gt;：14.x 的小版本更新可以装，但下载的是完整安装包；更新完显卡等补丁会失效，OCLP 会自动弹窗提示重新打补丁。大版本升级（15、26）别点。我关掉了”自动安装 macOS 更新”，只保留”安全响应和系统文件”。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;安全性略低&lt;/strong&gt;：为了打补丁，OCLP 关掉了部分系统完整性保护（SIP），个别检测 SIP 的软件可能拒绝运行。不在这台机器上处理敏感事务就好。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;FileVault 尽量别开&lt;/strong&gt;：打补丁时一般要求它关闭。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;系统不睡眠&lt;/strong&gt;：这台机器睡眠后唤醒会死机，现在是「只关屏幕、系统不睡」，详见上面的&lt;a href=&quot;#%E7%9D%A1%E7%9C%A0%E5%90%8E%E5%94%A4%E9%86%92%E5%B0%B1%E6%AD%BB%E6%9C%BA&quot;&gt;睡眠后唤醒就死机&lt;/a&gt;。别点苹果菜单里的「睡眠」。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Homebrew 没有现成的安装包&lt;/strong&gt;：Intel 芯片 + Sonoma 的组合，Homebrew 装软件要从源码编译，很慢。在这台机器上搭博客写作环境的过程记在了&lt;a href=&quot;/posts/hexo-to-astro-migration/&quot;&gt;《从 Hexo 迁移到 Astro》&lt;/a&gt;里。&lt;/li&gt;
&lt;/ul&gt;&lt;/section&gt;
&lt;section&gt;&lt;h2&gt;七、效果&lt;a href=&quot;#七效果&quot;&gt;&lt;span&gt;#&lt;/span&gt;&lt;/a&gt;&lt;/h2&gt;&lt;p&gt;升级后的系统是 macOS Sonoma 14.8.9。显卡、Wi-Fi、Touch Bar 都正常，只有 Touch ID 没有恢复，另外系统睡眠后无法唤醒，只能设成不睡眠、只关屏幕。新软件都能装了，接 4K 显示器看视频、写写代码、跑博客的本地预览都没问题，当一台家庭备用机绰绰有余。&lt;/p&gt;&lt;p&gt;一台十年前的电脑，硬件其实还够用，被淘汰的只是系统支持。OCLP 让它多用好几年。&lt;/p&gt;&lt;blockquote&gt;&lt;p&gt;参考：&lt;a href=&quot;https://dortania.github.io/OpenCore-Legacy-Patcher/SONOMA-DROP.html&quot; target=&quot;_blank&quot;&gt;OCLP 官方文档：macOS Sonoma&lt;/a&gt;、&lt;a href=&quot;https://dortania.github.io/OpenCore-Legacy-Patcher/MODELS.html&quot; target=&quot;_blank&quot;&gt;OCLP 支持机型&lt;/a&gt;、&lt;a href=&quot;https://github.com/dortania/OpenCore-Legacy-Patcher/releases&quot; target=&quot;_blank&quot;&gt;OCLP Releases&lt;/a&gt;、&lt;a href=&quot;https://hexacera.com/posts/oclp-sonoma-sleep-wake-failure&quot; target=&quot;_blank&quot;&gt;2017 款 MacBook Pro 在 OCLP Sonoma 上的睡眠唤醒失败&lt;/a&gt;。&lt;/p&gt;&lt;/blockquote&gt;&lt;/section&gt;</content:encoded></item><item><title>让医生别闲着：用 Python 协程把成本数据产出提速十倍</title><link>https://tracerzzz.cn/posts/python-asyncio-concurrent-queries/</link><guid isPermaLink="true">https://tracerzzz.cn/posts/python-asyncio-concurrent-queries/</guid><description>在百度云 CDN 做成本分摊时，日均要对 Palo 发 80 万次以上的查询，大部分时间都花在等结果上。这篇用&quot;医院看病&quot;做类比讲清楚异步非阻塞，顺带对比多进程、多线程、协程三种并发方式，最后讲我先用单进程协程（asyncio + aiomysql）并发查询，再加上多进程、用队列汇总结果，把数据产出时间缩短到原来的十分之一的过程；最后对比浏览器、Node.js 和 Python asyncio 的事件循环调度原理。</description><pubDate>Sun, 25 Oct 2026 14:00:00 GMT</pubDate><content:encoded>&lt;p&gt;&lt;/p&gt;&lt;figure&gt;&lt;img alt=&quot;让医生别闲着：一个诊室里一位医生在看病，旁边一排病人候诊；一位病人拿着 await 的单子去检验科，结果出来后再回来排队&quot; loading=&quot;lazy&quot; width=&quot;1920&quot; height=&quot;1080&quot; src=&quot;/_astro/clinic.qibTYcSa_Z2t5WSX.webp&quot; /&gt;&lt;figcaption&gt;让医生别闲着：一个诊室里一位医生在看病，旁边一排病人候诊；一位病人拿着 await 的单子去检验科，结果出来后再回来排队&lt;/figcaption&gt;&lt;/figure&gt;&lt;p&gt;&lt;/p&gt;
&lt;p&gt;在百度云 CDN 时，我负责成本优化的数据产出：把上百个节点的带宽成本，分摊到十万多个用户、再细到每个域名，每天产出给成本看板用。这件事日均要对 Palo（也就是现在的 Apache Doris）发 80 万次以上的查询。&lt;/p&gt;
&lt;p&gt;一开始我用单进程加协程并发发查询，后来又加上了&lt;strong&gt;多进程&lt;/strong&gt;：每个进程里跑一个事件循环，用协程并发发查询，各进程的结果通过队列汇总，产出时间缩短到原来的十分之一左右。&lt;/p&gt;
&lt;p&gt;协程这个思路我并不是在百度才想到的。早在中国电信写前端 JavaScript 的时候，我就琢磨过异步：浏览器里发一个请求出去，页面不能卡住等它回来，得先去干别的，等结果到了再回头处理。后来在百度遇到”大量查询、大部分时间在等”的问题，第一反应就是：这不就是同一件事吗。&lt;/p&gt;
&lt;blockquote&gt;&lt;p&gt;说明：这篇是 2026 年根据当年的项目回忆整理的。具体代码已经记不清，下面的代码是按通用做法写的示例，用来说明思路，不是原始实现。&lt;/p&gt;&lt;p&gt;2026-10-30 更新：修正了并发方案的描述（实际是单进程协程，后来加上多进程、用队列汇总结果，没有用多线程）；第一节补充了诊所运转图；第五节补充了浏览器、Node.js 和 Python asyncio 事件循环的调度原理。&lt;/p&gt;&lt;/blockquote&gt;
&lt;section&gt;&lt;h2&gt;一、医院里的异步&lt;a href=&quot;#一医院里的异步&quot;&gt;&lt;span&gt;#&lt;/span&gt;&lt;/a&gt;&lt;/h2&gt;&lt;p&gt;我自己理解异步，用的是医院看病的类比：&lt;/p&gt;&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;医生&lt;/strong&gt;是 CPU，负责”看”和”判断”；&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;病人&lt;/strong&gt;是一个个任务；&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;抽血、核磁、B 超&lt;/strong&gt;这些检查科室，是读文件、查数据库、调接口这类&lt;strong&gt;需要等&lt;/strong&gt;的事情。&lt;/li&gt;
&lt;/ul&gt;&lt;p&gt;一个病人看病的过程，大概是：医生问诊 → 开检查单 → 病人去做检查 → 拿着结果回来 → 医生看结果、下诊断。&lt;/p&gt;&lt;section&gt;&lt;h3&gt;同步阻塞：医生陪着病人去做检查&lt;a href=&quot;#同步阻塞医生陪着病人去做检查&quot;&gt;&lt;span&gt;#&lt;/span&gt;&lt;/a&gt;&lt;/h3&gt;&lt;p&gt;如果医生开完检查单，亲自陪病人去抽血，在化验室门口站着等结果出来，再一起回诊室，那么整个等待期间，医生什么也没干。后面排队的病人只能干等。&lt;/p&gt;&lt;p&gt;这就是&lt;strong&gt;同步阻塞&lt;/strong&gt;：程序发出一次查询，就停在那里等结果，等到了才发下一次。一次查询如果 1 秒，其中 0.95 秒都在等数据库返回，那 CPU 有 95% 的时间是闲着的。&lt;/p&gt;&lt;/section&gt;&lt;section&gt;&lt;h3&gt;异步非阻塞：开完单子就叫下一位&lt;a href=&quot;#异步非阻塞开完单子就叫下一位&quot;&gt;&lt;span&gt;#&lt;/span&gt;&lt;/a&gt;&lt;/h3&gt;&lt;p&gt;正常医院不是这样运转的。医生开完检查单，病人自己去排队做检查，医生马上叫下一位。等前面病人的检查结果出来了，医生看完手上这一位，再把他叫回来接着看。&lt;/p&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;异步&lt;/strong&gt;：结果好了会有通知（叫号），再回头接着处理。&lt;/li&gt;
&lt;/ul&gt;&lt;p&gt;医生始终在看病人，没有在等。病人多、检查多的时候，效率差别非常大。&lt;/p&gt;&lt;/section&gt;&lt;section&gt;&lt;h3&gt;对应到代码里&lt;a href=&quot;#对应到代码里&quot;&gt;&lt;span&gt;#&lt;/span&gt;&lt;/a&gt;&lt;/h3&gt;

&lt;table&gt;&lt;thead&gt;&lt;tr&gt;&lt;th&gt;医院&lt;/th&gt;&lt;th&gt;asyncio&lt;/th&gt;&lt;/tr&gt;&lt;/thead&gt;&lt;tbody&gt;&lt;tr&gt;&lt;td&gt;医生&lt;/td&gt;&lt;td&gt;CPU（一个线程）&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;病人&lt;/td&gt;&lt;td&gt;协程（coroutine），一个个 &lt;code&gt;async def&lt;/code&gt; 函数&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;开检查单，让病人去排队&lt;/td&gt;&lt;td&gt;&lt;code&gt;await&lt;/code&gt;：发起一个要等的操作，把控制权交出去&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;医生自己叫号&lt;/td&gt;&lt;td&gt;事件循环（event loop）：谁的结果好了就切回谁&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;检查科室同时能接待几个人&lt;/td&gt;&lt;td&gt;并发上限（连接池大小、信号量）&lt;/td&gt;&lt;/tr&gt;&lt;/tbody&gt;&lt;/table&gt;&lt;p&gt;要注意，这家诊所&lt;strong&gt;没有专门的分诊护士&lt;/strong&gt;：事件循环和协程跑在同一个线程上。医生看完一个病人，自己看看候诊区、接一下检验科的电话，再叫下一位；候诊区空了，就守着电话等。所以医生一旦被某个病人拖住，连叫号都没人做了。&lt;/p&gt;&lt;p&gt;把整家诊所画出来，每个位置都标上了对应的模块，Python 和 JavaScript 都有：&lt;/p&gt;&lt;p&gt;&lt;/p&gt;&lt;figure&gt;&lt;img alt=&quot;诊所运转图：一个诊室是一个线程，候诊区是就绪队列，医生是执行代码的线程，看病人是执行协程，两个病人之间自己叫号是事件循环，唯一的笔是 GIL；检查科室里，结果通知中心是 epoll（浏览器里是浏览器的其他线程），Palo 是真正做检查的地方&quot; loading=&quot;lazy&quot; width=&quot;960&quot; height=&quot;500&quot; src=&quot;/_astro/asyncio-clinic.DuGzrikQ_Z1OzHGx.svg&quot; /&gt;&lt;figcaption&gt;诊所运转图：一个诊室是一个线程，候诊区是就绪队列，医生是执行代码的线程，看病人是执行协程，两个病人之间自己叫号是事件循环，唯一的笔是 GIL；检查科室里，结果通知中心是 epoll（浏览器里是浏览器的其他线程），Palo 是真正做检查的地方&lt;/figcaption&gt;&lt;/figure&gt;&lt;p&gt;&lt;/p&gt;&lt;p&gt;有一个关键前提：&lt;strong&gt;病人得自己去排队，不能拖着医生走&lt;/strong&gt;。如果某个”检查”是医生必须亲自陪着做的（在协程里调用了一个同步阻塞的函数），医生就又被拖住了，后面所有人都得等。这一点后面讲代码时还会提到。&lt;/p&gt;&lt;/section&gt;&lt;/section&gt;
&lt;section&gt;&lt;h2&gt;二、并发的基础：进程、线程、协程&lt;a href=&quot;#二并发的基础进程线程协程&quot;&gt;&lt;span&gt;#&lt;/span&gt;&lt;/a&gt;&lt;/h2&gt;&lt;p&gt;在讲项目之前，先把几种并发方式捋一下。&lt;/p&gt;&lt;section&gt;&lt;h3&gt;并发和并行&lt;a href=&quot;#并发和并行&quot;&gt;&lt;span&gt;#&lt;/span&gt;&lt;/a&gt;&lt;/h3&gt;&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;并发&lt;/strong&gt;（concurrency）：一段时间内同时处理多件事，但不一定在同一时刻都在做。一个医生轮流看好几个病人，是并发。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;并行&lt;/strong&gt;（parallelism）：同一时刻真的在同时做多件事。好几个医生各看各的病人，是并行。&lt;/li&gt;
&lt;/ul&gt;&lt;p&gt;并行需要多个 CPU 核心；并发一个核心也能做到。&lt;/p&gt;&lt;/section&gt;&lt;section&gt;&lt;h3&gt;三种方式&lt;a href=&quot;#三种方式&quot;&gt;&lt;span&gt;#&lt;/span&gt;&lt;/a&gt;&lt;/h3&gt;&lt;p&gt;&lt;strong&gt;多进程&lt;/strong&gt;：开好几个诊室，每个诊室一个医生，各自有一套病历柜。&lt;/p&gt;&lt;ul&gt;
&lt;li&gt;进程是操作系统分配资源的单位，每个进程有独立的内存空间。&lt;/li&gt;
&lt;li&gt;进程之间互不干扰，一个崩了不影响别的；但要交换数据就得通过管道、队列、共享内存这类方式，比较麻烦。&lt;/li&gt;
&lt;li&gt;创建和切换的开销最大，能真正利用多核并行。&lt;/li&gt;
&lt;/ul&gt;&lt;p&gt;&lt;strong&gt;多线程&lt;/strong&gt;：一个诊室里有好几个医生，共用一套病历柜。&lt;/p&gt;&lt;ul&gt;
&lt;li&gt;线程是操作系统调度的单位，同一进程里的线程共享内存。&lt;/li&gt;
&lt;li&gt;共享数据很方便，但多个线程同时改同一份数据会出问题，需要加锁。&lt;/li&gt;
&lt;li&gt;线程由操作系统调度，&lt;strong&gt;抢占式&lt;/strong&gt;切换：操作系统说切就切，线程自己控制不了。&lt;/li&gt;
&lt;/ul&gt;&lt;p&gt;&lt;strong&gt;协程&lt;/strong&gt;：只有一个医生，但他自己安排看病顺序，病人去做检查时就先看下一个。&lt;/p&gt;&lt;ul&gt;
&lt;li&gt;协程是用户态的”轻量级线程”，由程序自己调度，操作系统不知道它的存在。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;协作式&lt;/strong&gt;切换：只在 &lt;code&gt;await&lt;/code&gt; 这种明确的地方切换，所以不会在一行代码执行到一半时被切走，大多数情况下不用加锁。&lt;/li&gt;
&lt;li&gt;一个协程只是一个小对象，开几万个也没压力；线程每个都要占一块栈内存，开几千个就吃力了。&lt;/li&gt;
&lt;li&gt;一个协程如果不主动让出（比如执行了一段很长的计算，或者调用了阻塞函数），其他协程就都得等着。&lt;/li&gt;
&lt;/ul&gt;&lt;/section&gt;&lt;section&gt;&lt;h3&gt;Python 的特殊情况：GIL&lt;a href=&quot;#python-的特殊情况gil&quot;&gt;&lt;span&gt;#&lt;/span&gt;&lt;/a&gt;&lt;/h3&gt;&lt;p&gt;Python（默认的 CPython 解释器）有一把&lt;strong&gt;全局解释器锁&lt;/strong&gt;（GIL）：同一时刻，一个进程里只有一个线程能执行 Python 代码。&lt;/p&gt;&lt;p&gt;套到医院里，就像一个诊室里虽然有好几个医生，但只有一支笔，谁拿到笔谁才能写病历。所以在 Python 里：&lt;/p&gt;&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;CPU 密集型任务&lt;/strong&gt;（大量计算）：多线程没有用，几个线程抢一支笔，还多了切换开销。要用&lt;strong&gt;多进程&lt;/strong&gt;，开好几个诊室，才能真正用上多核。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;IO 密集型任务&lt;/strong&gt;（大量等待）：线程在等 IO 的时候会把 GIL 让出来，所以多线程是有用的；协程也有用，而且更轻。&lt;/li&gt;
&lt;/ul&gt;&lt;blockquote&gt;&lt;p&gt;Python 3.13 开始提供了可选的去掉 GIL 的构建版本，但默认版本仍然有 GIL，上面的结论在大多数环境里依然成立。&lt;/p&gt;&lt;/blockquote&gt;&lt;/section&gt;&lt;section&gt;&lt;h3&gt;对比&lt;a href=&quot;#对比&quot;&gt;&lt;span&gt;#&lt;/span&gt;&lt;/a&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;谁来调度&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;切换开销&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;数据安全&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;Python 里受 GIL 限制&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;IO 密集，且要用同步库&lt;/td&gt;&lt;td&gt;大量 IO 密集任务&lt;/td&gt;&lt;/tr&gt;&lt;/tbody&gt;&lt;/table&gt;&lt;p&gt;实际项目里经常组合使用：多进程把多核用满，每个进程里再跑一个事件循环处理大量 IO。&lt;/p&gt;&lt;/section&gt;&lt;/section&gt;
&lt;section&gt;&lt;h2&gt;三、项目里的问题：80 万次查询，大部分时间在等&lt;a href=&quot;#三项目里的问题80-万次查询大部分时间在等&quot;&gt;&lt;span&gt;#&lt;/span&gt;&lt;/a&gt;&lt;/h2&gt;&lt;p&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;指标多。&lt;/strong&gt; 带宽、流量、请求数、各计费方式的计费值，每个都要单独算。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;存储分散。&lt;/strong&gt; 数据分布在不同的表、不同的库里，没办法一条 SQL 全拿出来。&lt;/li&gt;
&lt;/ul&gt;&lt;p&gt;结果就是：一天要对 Palo 发 80 万次以上的查询。每次查询本身并不复杂，但网络往返加上等 Palo 返回结果，总要等上一会儿。如果一条一条查，大部分时间都是程序在”站在化验室门口等”。&lt;/p&gt;&lt;p&gt;这正是协程最适合的场景：&lt;strong&gt;任务很多，彼此独立，每个任务大部分时间在等 IO。&lt;/strong&gt;&lt;/p&gt;&lt;section&gt;&lt;h3&gt;优化分两半&lt;a href=&quot;#优化分两半&quot;&gt;&lt;span&gt;#&lt;/span&gt;&lt;/a&gt;&lt;/h3&gt;&lt;p&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;让医生不闲着&lt;/strong&gt;：用协程并发发查询，在等一个结果的时候去发别的查询。&lt;/li&gt;
&lt;/ul&gt;&lt;p&gt;这篇只讲第二部分。&lt;/p&gt;&lt;/section&gt;&lt;section&gt;&lt;h3&gt;为什么没用多线程&lt;a href=&quot;#为什么没用多线程&quot;&gt;&lt;span&gt;#&lt;/span&gt;&lt;/a&gt;&lt;/h3&gt;&lt;p&gt;说到并发，Python 里最先想到的通常是多线程。查询是 IO 密集的，线程等结果时会让出 GIL，理论上是有用的。但当时我已经知道 GIL 的存在，没有走多线程这条路，一开始就选了协程。&lt;/p&gt;&lt;p&gt;写这篇文章时，我把多线程为什么不合适的账重新算了一遍，主要是下面三点。&lt;/p&gt;&lt;p&gt;&lt;strong&gt;1. 一个线程同一时刻只有一个查询在路上。&lt;/strong&gt;&lt;/p&gt;&lt;p&gt;线程发出查询后就阻塞在那里，直到结果回来才能发下一个。所以多线程的并发度就等于线程数。&lt;/p&gt;&lt;p&gt;可以算一笔账（数字是举例，不是当时的实测值）：假设一次查询从发出到拿到结果平均要 0.5 秒，开 50 个线程，每秒最多完成&lt;/p&gt;&lt;span&gt;&lt;span&gt;&lt;span&gt;同时在路上的查询数单次耗时=500.5 s=100 次/秒\frac{\text{同时在路上的查询数}}{\text{单次耗时}} = \frac{50}{0.5\,\text{s}} = 100\ \text{次/秒}&lt;/span&gt;&lt;span&gt;&lt;span&gt;&lt;span&gt;&lt;/span&gt;&lt;span&gt;&lt;span&gt;&lt;/span&gt;&lt;span&gt;&lt;span&gt;&lt;span&gt;&lt;span&gt;&lt;span&gt;&lt;span&gt;&lt;/span&gt;&lt;span&gt;&lt;span&gt;&lt;span&gt;单次耗时&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span&gt;&lt;span&gt;&lt;/span&gt;&lt;span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span&gt;&lt;span&gt;&lt;/span&gt;&lt;span&gt;&lt;span&gt;&lt;span&gt;同时在路上的查询数&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span&gt;​&lt;/span&gt;&lt;/span&gt;&lt;span&gt;&lt;span&gt;&lt;span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span&gt;&lt;/span&gt;&lt;span&gt;=&lt;/span&gt;&lt;span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span&gt;&lt;span&gt;&lt;/span&gt;&lt;span&gt;&lt;span&gt;&lt;/span&gt;&lt;span&gt;&lt;span&gt;&lt;span&gt;&lt;span&gt;&lt;span&gt;&lt;span&gt;&lt;/span&gt;&lt;span&gt;&lt;span&gt;0.5&lt;/span&gt;&lt;span&gt;&lt;/span&gt;&lt;span&gt;&lt;span&gt;s&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span&gt;&lt;span&gt;&lt;/span&gt;&lt;span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span&gt;&lt;span&gt;&lt;/span&gt;&lt;span&gt;&lt;span&gt;50&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span&gt;​&lt;/span&gt;&lt;/span&gt;&lt;span&gt;&lt;span&gt;&lt;span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span&gt;&lt;/span&gt;&lt;span&gt;=&lt;/span&gt;&lt;span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span&gt;&lt;span&gt;&lt;/span&gt;&lt;span&gt;100&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;&lt;span&gt;次&lt;/span&gt;&lt;span&gt;/&lt;/span&gt;&lt;span&gt;秒&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;p&gt;80 万次查询就要 8000 秒，两个多小时。想再快，只能把同时在路上的查询变多，也就是加线程。&lt;/p&gt;&lt;p&gt;&lt;strong&gt;2. 线程加不上去。&lt;/strong&gt;&lt;/p&gt;&lt;p&gt;每个线程都要占一块栈内存（Linux 上默认按 8MB 预留虚拟内存），由操作系统调度。线程开到几百个时，创建、切换、调度本身就成了负担，加线程的收益越来越小。&lt;/p&gt;&lt;p&gt;&lt;strong&gt;3. 线程越多，GIL 抢得越凶。&lt;/strong&gt;&lt;/p&gt;&lt;p&gt;等 IO 的时候 GIL 会让出来，可查询结果回来之后，解析结果、组装数据、做计算都是 Python 代码，必须拿到 GIL 才能执行。常用的 MySQL 驱动 PyMySQL 是纯 Python 实现的（aiomysql 底层用的也是它），连解析 MySQL 协议、把返回的字节转成 Python 对象都要占着 GIL。成本数据的查询结果往往不小，这部分开销并不低。&lt;/p&gt;&lt;p&gt;线程一多，大家的结果差不多同时回来，排队等同一把锁；Python 还会每隔一小段时间强制让持锁的线程交出 GIL，切换越频繁，浪费越多。结果是加了线程，真正干活的时间却没有成比例增加。&lt;/p&gt;&lt;p&gt;用医院来说：多线程是多请了几个医生，但每个医生还是陪着自己的病人去做检查，一次只能陪一个；医生多了，诊室里那支唯一的笔又不够用，大家先在抢笔。&lt;/p&gt;&lt;p&gt;协程正好解决第 1、2 点：一个线程里可以同时有几十、上百个查询在路上，而协程只是一个小对象，开多少都不会给操作系统添负担。第 3 点协程解决不了，这也是后来又加上多进程的原因。&lt;/p&gt;&lt;/section&gt;&lt;/section&gt;
&lt;section&gt;&lt;h2&gt;四、asyncio + aiomysql&lt;a href=&quot;#四asyncio--aiomysql&quot;&gt;&lt;span&gt;#&lt;/span&gt;&lt;/a&gt;&lt;/h2&gt;&lt;p&gt;Palo 兼容 MySQL 协议，可以直接用 MySQL 客户端连。要在协程里查，就得用异步的 MySQL 客户端：&lt;strong&gt;aiomysql&lt;/strong&gt;。&lt;/p&gt;&lt;p&gt;这里正好对应前面说的那个前提：如果在协程里用普通的 &lt;code&gt;pymysql&lt;/code&gt;，执行查询时会阻塞整个线程，相当于医生又亲自陪病人去抽血了，所有协程都会被卡住，并发效果为零。aiomysql 在等结果时会把控制权交还给事件循环，这才是”病人自己去排队”。&lt;/p&gt;&lt;p&gt;一个简化的示例：&lt;/p&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;&lt;figure&gt;&lt;figcaption&gt;&lt;/figcaption&gt;&lt;pre&gt;&lt;code&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;1&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;import&lt;/span&gt;&lt;span&gt; asyncio&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;2&lt;/div&gt;&lt;/div&gt;&lt;div&gt;
&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;3&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;import&lt;/span&gt;&lt;span&gt; aiomysql&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;4&lt;/div&gt;&lt;/div&gt;&lt;div&gt;
&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;5&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;&lt;span&gt;CONCURRENCY&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;=&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;/span&gt;&lt;span&gt;50&lt;/span&gt;&lt;span&gt;  &lt;/span&gt;&lt;span&gt;# 同时最多跑多少个查询，按 Palo 监控调&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;6&lt;/div&gt;&lt;/div&gt;&lt;div&gt;
&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;7&lt;/div&gt;&lt;/div&gt;&lt;div&gt;
&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;8&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;async&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;def&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;run_query&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;pool&lt;/span&gt;&lt;span&gt;,&lt;/span&gt;&lt;span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;sem&lt;/span&gt;&lt;/span&gt;&lt;span&gt;,&lt;/span&gt;&lt;span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;sql&lt;/span&gt;&lt;/span&gt;&lt;span&gt;,&lt;/span&gt;&lt;span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;params&lt;/span&gt;&lt;/span&gt;&lt;span&gt;):&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;9&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;    &lt;/span&gt;&lt;span&gt;async&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;with&lt;/span&gt;&lt;span&gt; sem:  &lt;/span&gt;&lt;span&gt;# 超过并发上限的查询在这里排队&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;10&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;        &lt;/span&gt;&lt;span&gt;async&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;with&lt;/span&gt;&lt;span&gt;&lt;span&gt; pool.&lt;/span&gt;&lt;span&gt;acquire&lt;/span&gt;&lt;span&gt;() &lt;/span&gt;&lt;/span&gt;&lt;span&gt;as&lt;/span&gt;&lt;span&gt; conn:&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;11&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;            &lt;/span&gt;&lt;span&gt;async&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;with&lt;/span&gt;&lt;span&gt;&lt;span&gt; conn.&lt;/span&gt;&lt;span&gt;cursor&lt;/span&gt;&lt;span&gt;(aiomysql.DictCursor) &lt;/span&gt;&lt;/span&gt;&lt;span&gt;as&lt;/span&gt;&lt;span&gt; cur:&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;12&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;                &lt;/span&gt;&lt;span&gt;await&lt;/span&gt;&lt;span&gt;&lt;span&gt; cur.&lt;/span&gt;&lt;span&gt;execute&lt;/span&gt;&lt;span&gt;(sql, params)  &lt;/span&gt;&lt;/span&gt;&lt;span&gt;# 交出控制权，等 Palo 返回&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;13&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;                &lt;/span&gt;&lt;span&gt;return&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;await&lt;/span&gt;&lt;span&gt;&lt;span&gt; cur.&lt;/span&gt;&lt;span&gt;fetchall&lt;/span&gt;&lt;span&gt;()&lt;/span&gt;&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;14&lt;/div&gt;&lt;/div&gt;&lt;div&gt;
&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;15&lt;/div&gt;&lt;/div&gt;&lt;div&gt;
&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;16&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;async&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;def&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;main&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;queries&lt;/span&gt;&lt;span&gt;):&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;17&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;&lt;span&gt;    &lt;/span&gt;&lt;/span&gt;&lt;span&gt;pool &lt;/span&gt;&lt;span&gt;=&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;await&lt;/span&gt;&lt;span&gt;&lt;span&gt; aiomysql.&lt;/span&gt;&lt;span&gt;create_pool&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;18&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;        &lt;/span&gt;&lt;span&gt;host&lt;/span&gt;&lt;span&gt;=&lt;/span&gt;&lt;span&gt;&quot;palo-fe.example&quot;&lt;/span&gt;&lt;span&gt;, &lt;/span&gt;&lt;span&gt;port&lt;/span&gt;&lt;span&gt;=&lt;/span&gt;&lt;span&gt;9030&lt;/span&gt;&lt;span&gt;,&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;19&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;        &lt;/span&gt;&lt;span&gt;user&lt;/span&gt;&lt;span&gt;=&lt;/span&gt;&lt;span&gt;&quot;reader&quot;&lt;/span&gt;&lt;span&gt;, &lt;/span&gt;&lt;span&gt;password&lt;/span&gt;&lt;span&gt;=&lt;/span&gt;&lt;span&gt;&quot;***&quot;&lt;/span&gt;&lt;span&gt;, &lt;/span&gt;&lt;span&gt;db&lt;/span&gt;&lt;span&gt;=&lt;/span&gt;&lt;span&gt;&quot;cost&quot;&lt;/span&gt;&lt;span&gt;,&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;20&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;        &lt;/span&gt;&lt;span&gt;minsize&lt;/span&gt;&lt;span&gt;=&lt;/span&gt;&lt;span&gt;1&lt;/span&gt;&lt;span&gt;, &lt;/span&gt;&lt;span&gt;maxsize&lt;/span&gt;&lt;span&gt;&lt;span&gt;=&lt;/span&gt;&lt;span&gt;CONCURRENCY&lt;/span&gt;&lt;span&gt;,&lt;/span&gt;&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;21&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;&lt;span&gt;    &lt;/span&gt;&lt;/span&gt;&lt;span&gt;)&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;22&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;&lt;span&gt;    &lt;/span&gt;&lt;/span&gt;&lt;span&gt;sem &lt;/span&gt;&lt;span&gt;=&lt;/span&gt;&lt;span&gt; asyncio.&lt;/span&gt;&lt;span&gt;Semaphore&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;CONCURRENCY&lt;/span&gt;&lt;span&gt;)&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;23&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;    &lt;/span&gt;&lt;span&gt;try&lt;/span&gt;&lt;span&gt;:&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;24&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;&lt;span&gt;        &lt;/span&gt;&lt;/span&gt;&lt;span&gt;tasks &lt;/span&gt;&lt;span&gt;=&lt;/span&gt;&lt;span&gt; [&lt;/span&gt;&lt;span&gt;run_query&lt;/span&gt;&lt;span&gt;(pool, sem, sql, params) &lt;/span&gt;&lt;span&gt;for&lt;/span&gt;&lt;span&gt; sql, params &lt;/span&gt;&lt;span&gt;in&lt;/span&gt;&lt;span&gt; queries]&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;25&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;        &lt;/span&gt;&lt;span&gt;return&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;await&lt;/span&gt;&lt;span&gt;&lt;span&gt; asyncio.&lt;/span&gt;&lt;span&gt;gather&lt;/span&gt;&lt;span&gt;(*tasks, &lt;/span&gt;&lt;/span&gt;&lt;span&gt;return_exceptions&lt;/span&gt;&lt;span&gt;=&lt;/span&gt;&lt;span&gt;True&lt;/span&gt;&lt;span&gt;)&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;26&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;    &lt;/span&gt;&lt;span&gt;finally&lt;/span&gt;&lt;span&gt;:&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;27&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;&lt;span&gt;        &lt;/span&gt;&lt;/span&gt;&lt;span&gt;pool.&lt;/span&gt;&lt;span&gt;close&lt;/span&gt;&lt;span&gt;()&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;28&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;        &lt;/span&gt;&lt;span&gt;await&lt;/span&gt;&lt;span&gt;&lt;span&gt; pool.&lt;/span&gt;&lt;span&gt;wait_closed&lt;/span&gt;&lt;span&gt;()&lt;/span&gt;&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;29&lt;/div&gt;&lt;/div&gt;&lt;div&gt;
&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;30&lt;/div&gt;&lt;/div&gt;&lt;div&gt;
&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;31&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;&lt;span&gt;results &lt;/span&gt;&lt;span&gt;=&lt;/span&gt;&lt;span&gt; asyncio.&lt;/span&gt;&lt;span&gt;run&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;main&lt;/span&gt;&lt;span&gt;(queries))&lt;/span&gt;&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;/code&gt;&lt;/pre&gt;&lt;div&gt;&lt;div&gt;&lt;/div&gt;&lt;div&gt;&lt;/div&gt;&lt;/div&gt;&lt;/figure&gt;&lt;div&gt;&lt;/div&gt;&lt;/div&gt;&lt;span&gt;展开&lt;/span&gt;&lt;span&gt;收起&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;p&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;信号量控制并发&lt;/strong&gt;：&lt;code&gt;asyncio.Semaphore&lt;/code&gt; 规定同时最多有多少个查询在跑，相当于检查科室同时能接待的人数。这里连接池的 &lt;code&gt;maxsize&lt;/code&gt; 也能起到同样的作用，写成信号量是为了让”并发上限”这件事更直观。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;&lt;code&gt;gather&lt;/code&gt; 收集结果&lt;/strong&gt;：所有查询一起提交，全部完成后按原顺序拿到结果。&lt;code&gt;return_exceptions=True&lt;/code&gt; 让单个查询失败不会中断其他查询，失败的再单独重试。&lt;/li&gt;
&lt;li&gt;查询数量特别多时，一次性创建几十万个协程对象会占不少内存。可以分批提交，或者改成”固定数量的 worker 从队列里取任务”的写法，效果一样。&lt;/li&gt;
&lt;/ul&gt;&lt;section&gt;&lt;h3&gt;从单进程到多进程&lt;a href=&quot;#从单进程到多进程&quot;&gt;&lt;span&gt;#&lt;/span&gt;&lt;/a&gt;&lt;/h3&gt;&lt;p&gt;一开始只有一个进程、一个事件循环，就是上面这段代码的样子。协程让同时在路上的查询从一个变成了几十个，“等”的问题解决了。&lt;/p&gt;&lt;p&gt;但接下来就碰到了前面说的第 3 点：查询结果回来以后，解析结果、整理数据、做计算都要执行 Python 代码，一个进程受 GIL 限制，最多只能用满一个核。查询并发越高，结果回来得越密集，这部分 CPU 工作就越成为瓶颈。&lt;/p&gt;&lt;p&gt;所以后来又加上了&lt;strong&gt;多进程&lt;/strong&gt;：每个进程有自己独立的解释器和 GIL，可以各自用满一个核；每个进程里再跑一个事件循环，用协程并发发查询。各进程的结果通过&lt;strong&gt;队列&lt;/strong&gt;（&lt;code&gt;multiprocessing.Queue&lt;/code&gt;）传回主进程汇总。&lt;/p&gt;&lt;p&gt;任务是按域名拆的：把全部域名列表按下标切成几段，每个进程负责一段域名；进程里再把这批域名要查的各项指标、各张表的查询，用协程并发跑完。整体结构如下：&lt;/p&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;&lt;/span&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;&lt;/span&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;&lt;/span&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;&lt;p&gt;第 1 段域名&lt;/p&gt;&lt;/span&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;&lt;p&gt;第 2 段域名&lt;/p&gt;&lt;/span&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;&lt;p&gt;第 N 段域名&lt;/p&gt;&lt;/span&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;&lt;p&gt;并发查询&lt;/p&gt;&lt;/span&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;&lt;p&gt;并发查询&lt;/p&gt;&lt;/span&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;&lt;p&gt;并发查询&lt;/p&gt;&lt;/span&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;&lt;p&gt;结果放入队列&lt;/p&gt;&lt;/span&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;&lt;p&gt;结果放入队列&lt;/p&gt;&lt;/span&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;&lt;p&gt;结果放入队列&lt;/p&gt;&lt;/span&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;&lt;p&gt;进程 N&lt;/p&gt;&lt;/span&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;&lt;/span&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;&lt;p&gt;事件循环&lt;/p&gt;&lt;/span&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;&lt;p&gt;协程 × 50&lt;br /&gt;&lt;br /&gt;aiomysql 连接池 + 信号量&lt;/p&gt;&lt;/span&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;&lt;p&gt;进程 2&lt;/p&gt;&lt;/span&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;&lt;/span&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;&lt;p&gt;事件循环&lt;/p&gt;&lt;/span&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;&lt;p&gt;协程 × 50&lt;br /&gt;&lt;br /&gt;aiomysql 连接池 + 信号量&lt;/p&gt;&lt;/span&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;&lt;p&gt;进程 1&lt;/p&gt;&lt;/span&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;&lt;/span&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;&lt;p&gt;事件循环&lt;/p&gt;&lt;/span&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;&lt;p&gt;协程 × 50&lt;br /&gt;&lt;br /&gt;aiomysql 连接池 + 信号量&lt;/p&gt;&lt;/span&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;&lt;p&gt;全部域名列表&lt;/p&gt;&lt;/span&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;&lt;p&gt;按下标切成 N 段&lt;/p&gt;&lt;/span&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;&lt;p&gt;Palo&lt;/p&gt;&lt;/span&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;&lt;p&gt;multiprocessing.Queue&lt;/p&gt;&lt;/span&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;&lt;p&gt;主进程汇总&lt;/p&gt;&lt;/span&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;&lt;p&gt;写入成本表，看板 T+1 产出&lt;/p&gt;&lt;/span&gt;&lt;/div&gt;
&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;&lt;/span&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;&lt;/span&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;&lt;/span&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;&lt;p&gt;第 1 段域名&lt;/p&gt;&lt;/span&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;&lt;p&gt;第 2 段域名&lt;/p&gt;&lt;/span&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;&lt;p&gt;第 N 段域名&lt;/p&gt;&lt;/span&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;&lt;p&gt;并发查询&lt;/p&gt;&lt;/span&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;&lt;p&gt;并发查询&lt;/p&gt;&lt;/span&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;&lt;p&gt;并发查询&lt;/p&gt;&lt;/span&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;&lt;p&gt;结果放入队列&lt;/p&gt;&lt;/span&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;&lt;p&gt;结果放入队列&lt;/p&gt;&lt;/span&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;&lt;p&gt;结果放入队列&lt;/p&gt;&lt;/span&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;&lt;p&gt;进程 N&lt;/p&gt;&lt;/span&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;&lt;/span&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;&lt;p&gt;事件循环&lt;/p&gt;&lt;/span&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;&lt;p&gt;协程 × 50&lt;br /&gt;&lt;br /&gt;aiomysql 连接池 + 信号量&lt;/p&gt;&lt;/span&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;&lt;p&gt;进程 2&lt;/p&gt;&lt;/span&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;&lt;/span&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;&lt;p&gt;事件循环&lt;/p&gt;&lt;/span&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;&lt;p&gt;协程 × 50&lt;br /&gt;&lt;br /&gt;aiomysql 连接池 + 信号量&lt;/p&gt;&lt;/span&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;&lt;p&gt;进程 1&lt;/p&gt;&lt;/span&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;&lt;/span&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;&lt;p&gt;事件循环&lt;/p&gt;&lt;/span&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;&lt;p&gt;协程 × 50&lt;br /&gt;&lt;br /&gt;aiomysql 连接池 + 信号量&lt;/p&gt;&lt;/span&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;&lt;p&gt;全部域名列表&lt;/p&gt;&lt;/span&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;&lt;p&gt;按下标切成 N 段&lt;/p&gt;&lt;/span&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;&lt;p&gt;Palo&lt;/p&gt;&lt;/span&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;&lt;p&gt;multiprocessing.Queue&lt;/p&gt;&lt;/span&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;&lt;p&gt;主进程汇总&lt;/p&gt;&lt;/span&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;&lt;p&gt;写入成本表，看板 T+1 产出&lt;/p&gt;&lt;/span&gt;&lt;/div&gt;
&lt;/div&gt;&lt;/div&gt;&lt;/div&gt;&lt;p&gt;对应的代码：&lt;/p&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;&lt;figure&gt;&lt;figcaption&gt;&lt;/figcaption&gt;&lt;pre&gt;&lt;code&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;1&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;import&lt;/span&gt;&lt;span&gt; multiprocessing &lt;/span&gt;&lt;span&gt;as&lt;/span&gt;&lt;span&gt; mp&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;2&lt;/div&gt;&lt;/div&gt;&lt;div&gt;
&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;3&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;&lt;span&gt;PROCESSES&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;=&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;/span&gt;&lt;span&gt;8&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;4&lt;/div&gt;&lt;/div&gt;&lt;div&gt;
&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;5&lt;/div&gt;&lt;/div&gt;&lt;div&gt;
&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;6&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;def&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;worker&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;domains&lt;/span&gt;&lt;span&gt;,&lt;/span&gt;&lt;span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;queue&lt;/span&gt;&lt;/span&gt;&lt;span&gt;):&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;7&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;&lt;span&gt;    &lt;/span&gt;&lt;/span&gt;&lt;span&gt;queries &lt;/span&gt;&lt;span&gt;=&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;build_queries&lt;/span&gt;&lt;span&gt;(domains)      &lt;/span&gt;&lt;span&gt;# 这批域名要查的所有指标、所有表&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;8&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;&lt;span&gt;    &lt;/span&gt;&lt;/span&gt;&lt;span&gt;results &lt;/span&gt;&lt;span&gt;=&lt;/span&gt;&lt;span&gt; asyncio.&lt;/span&gt;&lt;span&gt;run&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;main&lt;/span&gt;&lt;span&gt;(queries))  &lt;/span&gt;&lt;span&gt;# 每个进程有自己的事件循环和连接池&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;9&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;&lt;span&gt;    &lt;/span&gt;&lt;/span&gt;&lt;span&gt;queue.&lt;/span&gt;&lt;span&gt;put&lt;/span&gt;&lt;span&gt;(results)                    &lt;/span&gt;&lt;span&gt;# 结果放进队列，交给主进程&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;10&lt;/div&gt;&lt;/div&gt;&lt;div&gt;
&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;11&lt;/div&gt;&lt;/div&gt;&lt;div&gt;
&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;12&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;if&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;__name__&lt;/span&gt;&lt;span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;==&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;/span&gt;&lt;span&gt;&quot;__main__&quot;&lt;/span&gt;&lt;span&gt;:&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;13&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;&lt;span&gt;    &lt;/span&gt;&lt;/span&gt;&lt;span&gt;size &lt;/span&gt;&lt;span&gt;=&lt;/span&gt;&lt;span&gt; (&lt;/span&gt;&lt;span&gt;len&lt;/span&gt;&lt;span&gt;&lt;span&gt;(domains) &lt;/span&gt;&lt;span&gt;+&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;PROCESSES&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;-&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;/span&gt;&lt;span&gt;1&lt;/span&gt;&lt;span&gt;&lt;span&gt;) &lt;/span&gt;&lt;span&gt;//&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;PROCESSES&lt;/span&gt;&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;14&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;&lt;span&gt;    &lt;/span&gt;&lt;/span&gt;&lt;span&gt;chunks &lt;/span&gt;&lt;span&gt;=&lt;/span&gt;&lt;span&gt; [domains[i:i &lt;/span&gt;&lt;span&gt;+&lt;/span&gt;&lt;span&gt; size] &lt;/span&gt;&lt;span&gt;for&lt;/span&gt;&lt;span&gt; i &lt;/span&gt;&lt;span&gt;in&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;range&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;0&lt;/span&gt;&lt;span&gt;, &lt;/span&gt;&lt;span&gt;len&lt;/span&gt;&lt;span&gt;(domains), size)]  &lt;/span&gt;&lt;span&gt;# 按下标切分&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;15&lt;/div&gt;&lt;/div&gt;&lt;div&gt;
&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;16&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;&lt;span&gt;    &lt;/span&gt;&lt;/span&gt;&lt;span&gt;queue &lt;/span&gt;&lt;span&gt;=&lt;/span&gt;&lt;span&gt; mp.&lt;/span&gt;&lt;span&gt;Queue&lt;/span&gt;&lt;span&gt;()&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;17&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;&lt;span&gt;    &lt;/span&gt;&lt;/span&gt;&lt;span&gt;procs &lt;/span&gt;&lt;span&gt;=&lt;/span&gt;&lt;span&gt; [mp.&lt;/span&gt;&lt;span&gt;Process&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;target&lt;/span&gt;&lt;span&gt;&lt;span&gt;=&lt;/span&gt;&lt;span&gt;worker, &lt;/span&gt;&lt;/span&gt;&lt;span&gt;args&lt;/span&gt;&lt;span&gt;&lt;span&gt;=&lt;/span&gt;&lt;span&gt;(chunk, queue)) &lt;/span&gt;&lt;/span&gt;&lt;span&gt;for&lt;/span&gt;&lt;span&gt; chunk &lt;/span&gt;&lt;span&gt;in&lt;/span&gt;&lt;span&gt; chunks]&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;18&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;    &lt;/span&gt;&lt;span&gt;for&lt;/span&gt;&lt;span&gt; p &lt;/span&gt;&lt;span&gt;in&lt;/span&gt;&lt;span&gt; procs:&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;19&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;&lt;span&gt;        &lt;/span&gt;&lt;/span&gt;&lt;span&gt;p.&lt;/span&gt;&lt;span&gt;start&lt;/span&gt;&lt;span&gt;()&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;20&lt;/div&gt;&lt;/div&gt;&lt;div&gt;
&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;21&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;&lt;span&gt;    &lt;/span&gt;&lt;/span&gt;&lt;span&gt;all_results &lt;/span&gt;&lt;span&gt;=&lt;/span&gt;&lt;span&gt; [queue.&lt;/span&gt;&lt;span&gt;get&lt;/span&gt;&lt;span&gt;() &lt;/span&gt;&lt;span&gt;for&lt;/span&gt;&lt;span&gt; _ &lt;/span&gt;&lt;span&gt;in&lt;/span&gt;&lt;span&gt; procs]  &lt;/span&gt;&lt;span&gt;# 先把结果都取出来&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;22&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;    &lt;/span&gt;&lt;span&gt;for&lt;/span&gt;&lt;span&gt; p &lt;/span&gt;&lt;span&gt;in&lt;/span&gt;&lt;span&gt; procs:&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;23&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;&lt;span&gt;        &lt;/span&gt;&lt;/span&gt;&lt;span&gt;p.&lt;/span&gt;&lt;span&gt;join&lt;/span&gt;&lt;span&gt;()                                 &lt;/span&gt;&lt;span&gt;# 再等子进程退出&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;/code&gt;&lt;/pre&gt;&lt;div&gt;&lt;div&gt;&lt;/div&gt;&lt;div&gt;&lt;/div&gt;&lt;/div&gt;&lt;/figure&gt;&lt;div&gt;&lt;/div&gt;&lt;/div&gt;&lt;span&gt;展开&lt;/span&gt;&lt;span&gt;收起&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;p&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;先取结果，再 &lt;code&gt;join&lt;/code&gt;&lt;/strong&gt;。子进程往队列里放了大量数据时，要等这些数据被取走才能真正退出。如果主进程先 &lt;code&gt;join&lt;/code&gt; 等子进程退出，子进程又在等数据被取走，两边就会互相卡住。Python 官方文档专门提醒过这一点。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;入口要放在 &lt;code&gt;if __name__ == &quot;__main__&quot;:&lt;/code&gt; 下面&lt;/strong&gt;。macOS 和 Windows 上默认用 spawn 方式创建子进程，子进程会重新导入主模块，不加这句会反复创建进程。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;进程数一般不超过 CPU 核数&lt;/strong&gt;。进程是为了用满多核，超过核数就只剩切换开销了。&lt;/li&gt;
&lt;/ul&gt;&lt;p&gt;还是用上面的账：8 个进程、每个进程 50 个并发，同时在路上的查询就是 400 个；而且结果解析和计算分到了 8 个核上并行，不再挤在一把 GIL 下排队。&lt;/p&gt;&lt;p&gt;回头看，有一处可以改进：队列里传的是 Python 对象，放进去时要序列化（pickle），主进程取出来再反序列化，结果数据量大时这部分开销不小，而且所有数据最后都压在主进程一个核上汇总。如果再做一次，我会让每个进程直接把自己那段域名的结果写进成本表，主进程只负责分配任务和确认是否都完成，省掉跨进程搬运大量数据。&lt;/p&gt;&lt;/section&gt;&lt;section&gt;&lt;h3&gt;并发开多大&lt;a href=&quot;#并发开多大&quot;&gt;&lt;span&gt;#&lt;/span&gt;&lt;/a&gt;&lt;/h3&gt;&lt;p&gt;并发不是越大越好。医生不闲着了，压力就转移到了检查科室：同时发太多查询，Palo 的 CPU、内存、查询队列会扛不住，查询反而变慢，甚至失败。&lt;/p&gt;&lt;p&gt;我的做法是&lt;strong&gt;看着 Palo 的监控调&lt;/strong&gt;：逐步加大并发，观察 Palo 的 CPU、内存和查询耗时，停在不触发告警线的位置。当时这套 Palo 是成本数据专用的服务器，不用担心影响其他业务，所以可以放心把它的余量用起来。&lt;/p&gt;&lt;/section&gt;&lt;section&gt;&lt;h3&gt;效果&lt;a href=&quot;#效果&quot;&gt;&lt;span&gt;#&lt;/span&gt;&lt;/a&gt;&lt;/h3&gt;&lt;p&gt;改成多进程 + 协程之后，数据产出时间缩短到原来的十分之一左右。&lt;/p&gt;&lt;p&gt;为什么是十倍，而不是并发数那么多倍？因为瓶颈转移了：&lt;/p&gt;&lt;ul&gt;
&lt;li&gt;查询并发起来以后，Palo 本身的处理能力成了上限，再加并发也不会更快；&lt;/li&gt;
&lt;li&gt;结果解析、计算、写库这些 CPU 工作虽然分到了多个进程，但进程数受核数限制，不可能无限扩。&lt;/li&gt;
&lt;/ul&gt;&lt;p&gt;这也说明，协程解决的是”等”的问题，多进程解决的是”算”的问题，两者各管一半，组合起来才把一台机器的能力用满。&lt;/p&gt;&lt;/section&gt;&lt;/section&gt;
&lt;section&gt;&lt;h2&gt;五、从 JavaScript 到 Python：事件循环是怎么调度的&lt;a href=&quot;#五从-javascript-到-python事件循环是怎么调度的&quot;&gt;&lt;span&gt;#&lt;/span&gt;&lt;/a&gt;&lt;/h2&gt;&lt;p&gt;最后说回开头。我在电信写前端时第一次认真想异步，是因为浏览器的 JavaScript 只有一个线程：页面渲染、响应点击、发请求都在这一个线程上。如果发一个请求就卡住等它回来，整个页面都会冻住。所以 JavaScript 从一开始就是异步的：先是回调函数，后来是 Promise，再后来是 &lt;code&gt;async&lt;/code&gt;/&lt;code&gt;await&lt;/code&gt;。&lt;/p&gt;&lt;p&gt;Python 的 asyncio 和它是同一套思想，连 &lt;code&gt;async&lt;/code&gt;/&lt;code&gt;await&lt;/code&gt; 这两个关键字都一样。但”同一套思想”具体是什么，每次循环里到底按什么顺序执行，当时我只有个模糊的印象。这一节把浏览器、Node.js 和 Python asyncio 的事件循环放在一起讲清楚。&lt;/p&gt;&lt;section&gt;&lt;h3&gt;共同的骨架&lt;a href=&quot;#共同的骨架&quot;&gt;&lt;span&gt;#&lt;/span&gt;&lt;/a&gt;&lt;/h3&gt;&lt;p&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;病人都在排队&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;/ul&gt;&lt;p&gt;区别主要在两点：&lt;strong&gt;谁负责”等”&lt;/strong&gt;，以及&lt;strong&gt;排队的规则&lt;/strong&gt;。&lt;/p&gt;&lt;/section&gt;&lt;section&gt;&lt;h3&gt;浏览器的事件循环&lt;a href=&quot;#浏览器的事件循环&quot;&gt;&lt;span&gt;#&lt;/span&gt;&lt;/a&gt;&lt;/h3&gt;&lt;p&gt;浏览器里，JavaScript 运行在一个主线程上，但浏览器本身是多线程的：定时器计时、网络请求、用户点击的监听，都由浏览器的其他线程负责。它们完成后，把对应的回调放进任务队列，等主线程来执行。&lt;/p&gt;&lt;p&gt;队列分两种：&lt;/p&gt;&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;宏任务&lt;/strong&gt;（task）：&lt;code&gt;setTimeout&lt;/code&gt;、&lt;code&gt;setInterval&lt;/code&gt; 的回调，网络请求完成、用户点击这些事件的回调，以及整段 &lt;code&gt;&amp;lt;script&amp;gt;&lt;/code&gt; 代码本身；&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;微任务&lt;/strong&gt;（microtask）：&lt;code&gt;Promise.then&lt;/code&gt;、&lt;code&gt;await&lt;/code&gt; 之后的代码、&lt;code&gt;queueMicrotask&lt;/code&gt;。&lt;/li&gt;
&lt;/ul&gt;&lt;p&gt;每一轮循环的规则是：&lt;/p&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;&lt;p&gt;完成后放入回调&lt;/p&gt;&lt;/span&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;&lt;/span&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;&lt;/span&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;&lt;/span&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;&lt;/span&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;&lt;p&gt;是&lt;/p&gt;&lt;/span&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;&lt;p&gt;否&lt;/p&gt;&lt;/span&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;&lt;/span&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;&lt;p&gt;浏览器的其他线程&lt;br /&gt;&lt;br /&gt;定时器、网络请求、用户事件&lt;/p&gt;&lt;/span&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;&lt;p&gt;宏任务队列&lt;/p&gt;&lt;/span&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;&lt;p&gt;取出一个宏任务&lt;/p&gt;&lt;/span&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;&lt;p&gt;执行到底，中途不会被打断&lt;/p&gt;&lt;/span&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;&lt;p&gt;清空微任务队列&lt;br /&gt;&lt;br /&gt;执行中新产生的微任务也一并执行完&lt;/p&gt;&lt;/span&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;&lt;p&gt;到渲染时机了吗？&lt;br /&gt;&lt;br /&gt;一般约每 16.7 毫秒一次&lt;/p&gt;&lt;/span&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;&lt;p&gt;执行 requestAnimationFrame 回调&lt;br /&gt;&lt;br /&gt;计算样式、布局、绘制&lt;/p&gt;&lt;/span&gt;&lt;/div&gt;
&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;&lt;p&gt;完成后放入回调&lt;/p&gt;&lt;/span&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;&lt;/span&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;&lt;/span&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;&lt;/span&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;&lt;/span&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;&lt;p&gt;是&lt;/p&gt;&lt;/span&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;&lt;p&gt;否&lt;/p&gt;&lt;/span&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;&lt;/span&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;&lt;p&gt;浏览器的其他线程&lt;br /&gt;&lt;br /&gt;定时器、网络请求、用户事件&lt;/p&gt;&lt;/span&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;&lt;p&gt;宏任务队列&lt;/p&gt;&lt;/span&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;&lt;p&gt;取出一个宏任务&lt;/p&gt;&lt;/span&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;&lt;p&gt;执行到底，中途不会被打断&lt;/p&gt;&lt;/span&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;&lt;p&gt;清空微任务队列&lt;br /&gt;&lt;br /&gt;执行中新产生的微任务也一并执行完&lt;/p&gt;&lt;/span&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;&lt;p&gt;到渲染时机了吗？&lt;br /&gt;&lt;br /&gt;一般约每 16.7 毫秒一次&lt;/p&gt;&lt;/span&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;&lt;p&gt;执行 requestAnimationFrame 回调&lt;br /&gt;&lt;br /&gt;计算样式、布局、绘制&lt;/p&gt;&lt;/span&gt;&lt;/div&gt;
&lt;/div&gt;&lt;/div&gt;&lt;/div&gt;&lt;p&gt;一个经典的例子：&lt;/p&gt;&lt;div&gt;&lt;figure&gt;&lt;figcaption&gt;&lt;/figcaption&gt;&lt;pre&gt;&lt;code&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;1&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;&lt;span&gt;console&lt;/span&gt;&lt;span&gt;.&lt;/span&gt;&lt;/span&gt;&lt;span&gt;log&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;&apos;1&apos;&lt;/span&gt;&lt;span&gt;);&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;2&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;setTimeout&lt;/span&gt;&lt;span&gt;(() &lt;/span&gt;&lt;span&gt;=&amp;gt;&lt;/span&gt;&lt;span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;console&lt;/span&gt;&lt;span&gt;.&lt;/span&gt;&lt;/span&gt;&lt;span&gt;log&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;&apos;4&apos;&lt;/span&gt;&lt;span&gt;), &lt;/span&gt;&lt;span&gt;0&lt;/span&gt;&lt;span&gt;);&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;3&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;Promise&lt;/span&gt;&lt;span&gt;.&lt;/span&gt;&lt;span&gt;resolve&lt;/span&gt;&lt;span&gt;().&lt;/span&gt;&lt;span&gt;then&lt;/span&gt;&lt;span&gt;(() &lt;/span&gt;&lt;span&gt;=&amp;gt;&lt;/span&gt;&lt;span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;console&lt;/span&gt;&lt;span&gt;.&lt;/span&gt;&lt;/span&gt;&lt;span&gt;log&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;&apos;3&apos;&lt;/span&gt;&lt;span&gt;));&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;4&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;&lt;span&gt;console&lt;/span&gt;&lt;span&gt;.&lt;/span&gt;&lt;/span&gt;&lt;span&gt;log&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;&apos;2&apos;&lt;/span&gt;&lt;span&gt;);&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;5&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;// 输出：1 2 3 4&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;/code&gt;&lt;/pre&gt;&lt;div&gt;&lt;div&gt;&lt;/div&gt;&lt;div&gt;&lt;/div&gt;&lt;/div&gt;&lt;/figure&gt;&lt;/div&gt;&lt;p&gt;整段脚本是第一个宏任务，先把同步代码执行完，打印 1 和 2；然后清空微任务队列，打印 3；&lt;code&gt;setTimeout&lt;/code&gt; 的回调是下一个宏任务，最后才打印 4。即使延迟写的是 0，它也要排到下一轮。&lt;/p&gt;&lt;p&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;微任务&lt;/strong&gt;是当前病人看完之前补开的单子，必须马上处理完，才能叫下一位；&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;渲染&lt;/strong&gt;是大厅的叫号屏，大约每 16.7 毫秒刷新一次，但只有医生手上空下来时才能刷新。&lt;/li&gt;
&lt;/ul&gt;&lt;p&gt;这就解释了前端最常见的卡顿：一段 JavaScript 执行太久，或者微任务一个接一个地产生停不下来，医生一直腾不出手，叫号屏就不刷新，页面看起来就冻住了。&lt;/p&gt;&lt;/section&gt;&lt;section&gt;&lt;h3&gt;Node.js 和浏览器一样吗&lt;a href=&quot;#nodejs-和浏览器一样吗&quot;&gt;&lt;span&gt;#&lt;/span&gt;&lt;/a&gt;&lt;/h3&gt;&lt;p&gt;&lt;strong&gt;不完全一样&lt;/strong&gt;。Node.js 是同一个思想，但实现不同：它的事件循环由 libuv 提供，负责”等”的不是浏览器的线程，而是操作系统。网络 IO 交给操作系统的 IO 多路复用（Linux 上是 epoll）；文件读写这类没法这样等的操作，交给 libuv 的一个后台线程池（默认 4 个线程）。&lt;/p&gt;&lt;p&gt;Node 的每一轮循环分成几个&lt;strong&gt;阶段&lt;/strong&gt;，按顺序轮转：&lt;/p&gt;&lt;ol&gt;
&lt;li&gt;&lt;strong&gt;timers&lt;/strong&gt;：执行到期的 &lt;code&gt;setTimeout&lt;/code&gt;、&lt;code&gt;setInterval&lt;/code&gt; 回调；&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;pending callbacks&lt;/strong&gt;：执行上一轮延后的部分 IO 回调；&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;poll&lt;/strong&gt;：通过 epoll 等待 IO 事件，并执行 IO 回调。等多久取决于最近的定时器和有没有 &lt;code&gt;setImmediate&lt;/code&gt;；&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;check&lt;/strong&gt;：执行 &lt;code&gt;setImmediate&lt;/code&gt; 的回调；&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;close callbacks&lt;/strong&gt;：执行连接关闭之类的回调。&lt;/li&gt;
&lt;/ol&gt;&lt;p&gt;（中间还有两个内部使用的阶段，平时接触不到，这里省略。）&lt;/p&gt;&lt;p&gt;微任务的规则和浏览器类似：每执行完一个回调，就先清空微任务队列。Node 多了一个 &lt;code&gt;process.nextTick&lt;/code&gt; 队列，它比 Promise 的微任务还优先。&lt;/p&gt;&lt;div&gt;&lt;figure&gt;&lt;figcaption&gt;&lt;/figcaption&gt;&lt;pre&gt;&lt;code&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;1&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;const&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;fs&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;=&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;require&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;&apos;fs&apos;&lt;/span&gt;&lt;span&gt;);&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;2&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;setTimeout&lt;/span&gt;&lt;span&gt;(() &lt;/span&gt;&lt;span&gt;=&amp;gt;&lt;/span&gt;&lt;span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;console&lt;/span&gt;&lt;span&gt;.&lt;/span&gt;&lt;/span&gt;&lt;span&gt;log&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;&apos;setTimeout&apos;&lt;/span&gt;&lt;span&gt;), &lt;/span&gt;&lt;span&gt;0&lt;/span&gt;&lt;span&gt;);&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;3&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;setImmediate&lt;/span&gt;&lt;span&gt;(() &lt;/span&gt;&lt;span&gt;=&amp;gt;&lt;/span&gt;&lt;span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;console&lt;/span&gt;&lt;span&gt;.&lt;/span&gt;&lt;/span&gt;&lt;span&gt;log&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;&apos;setImmediate&apos;&lt;/span&gt;&lt;span&gt;));&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;4&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;Promise&lt;/span&gt;&lt;span&gt;.&lt;/span&gt;&lt;span&gt;resolve&lt;/span&gt;&lt;span&gt;().&lt;/span&gt;&lt;span&gt;then&lt;/span&gt;&lt;span&gt;(() &lt;/span&gt;&lt;span&gt;=&amp;gt;&lt;/span&gt;&lt;span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;console&lt;/span&gt;&lt;span&gt;.&lt;/span&gt;&lt;/span&gt;&lt;span&gt;log&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;&apos;Promise.then&apos;&lt;/span&gt;&lt;span&gt;));&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;5&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;&lt;span&gt;process&lt;/span&gt;&lt;span&gt;.&lt;/span&gt;&lt;/span&gt;&lt;span&gt;nextTick&lt;/span&gt;&lt;span&gt;(() &lt;/span&gt;&lt;span&gt;=&amp;gt;&lt;/span&gt;&lt;span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;console&lt;/span&gt;&lt;span&gt;.&lt;/span&gt;&lt;/span&gt;&lt;span&gt;log&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;&apos;nextTick&apos;&lt;/span&gt;&lt;span&gt;));&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;6&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;&lt;span&gt;fs&lt;/span&gt;&lt;span&gt;.&lt;/span&gt;&lt;/span&gt;&lt;span&gt;readFile&lt;/span&gt;&lt;span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;__filename&lt;/span&gt;&lt;span&gt;, () &lt;/span&gt;&lt;/span&gt;&lt;span&gt;=&amp;gt;&lt;/span&gt;&lt;span&gt; {&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;7&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;  &lt;/span&gt;&lt;span&gt;setTimeout&lt;/span&gt;&lt;span&gt;(() &lt;/span&gt;&lt;span&gt;=&amp;gt;&lt;/span&gt;&lt;span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;console&lt;/span&gt;&lt;span&gt;.&lt;/span&gt;&lt;/span&gt;&lt;span&gt;log&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;&apos;IO 回调里的 setTimeout&apos;&lt;/span&gt;&lt;span&gt;), &lt;/span&gt;&lt;span&gt;0&lt;/span&gt;&lt;span&gt;);&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;8&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;  &lt;/span&gt;&lt;span&gt;setImmediate&lt;/span&gt;&lt;span&gt;(() &lt;/span&gt;&lt;span&gt;=&amp;gt;&lt;/span&gt;&lt;span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;console&lt;/span&gt;&lt;span&gt;.&lt;/span&gt;&lt;/span&gt;&lt;span&gt;log&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;&apos;IO 回调里的 setImmediate&apos;&lt;/span&gt;&lt;span&gt;));&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;9&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;});&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;/code&gt;&lt;/pre&gt;&lt;div&gt;&lt;div&gt;&lt;/div&gt;&lt;div&gt;&lt;/div&gt;&lt;/div&gt;&lt;/figure&gt;&lt;/div&gt;&lt;p&gt;我在 Node 22 上跑的输出是：&lt;/p&gt;&lt;div&gt;&lt;figure&gt;&lt;figcaption&gt;&lt;/figcaption&gt;&lt;pre&gt;&lt;code&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;1&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;nextTick&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;2&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;Promise.then&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;3&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;setTimeout&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;4&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;setImmediate&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;5&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;IO 回调里的 setImmediate&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;6&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;IO 回调里的 setTimeout&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;/code&gt;&lt;/pre&gt;&lt;div&gt;&lt;div&gt;&lt;/div&gt;&lt;div&gt;&lt;/div&gt;&lt;/div&gt;&lt;/figure&gt;&lt;/div&gt;&lt;ul&gt;
&lt;li&gt;&lt;code&gt;nextTick&lt;/code&gt; 和 &lt;code&gt;Promise.then&lt;/code&gt; 在主代码执行完后马上执行，&lt;code&gt;nextTick&lt;/code&gt; 在前；&lt;/li&gt;
&lt;li&gt;主代码里的 &lt;code&gt;setTimeout&lt;/code&gt; 和 &lt;code&gt;setImmediate&lt;/code&gt; 谁先谁后，Node 官方文档说是&lt;strong&gt;不保证&lt;/strong&gt;的，取决于进入循环时定时器是否已经到期；&lt;/li&gt;
&lt;li&gt;在 IO 回调里就是确定的：IO 回调在 poll 阶段执行，接下来就是 check 阶段，所以 &lt;code&gt;setImmediate&lt;/code&gt; 一定先于下一轮 timers 阶段的 &lt;code&gt;setTimeout&lt;/code&gt;。&lt;/li&gt;
&lt;/ul&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;Node.js&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;操作系统（epoll），文件 IO 用线程池&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;Promise、&lt;code&gt;queueMicrotask&lt;/code&gt;&lt;/td&gt;&lt;td&gt;同上，另有更优先的 &lt;code&gt;process.nextTick&lt;/code&gt;&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;特有的 API&lt;/td&gt;&lt;td&gt;&lt;code&gt;requestAnimationFrame&lt;/code&gt;，有渲染步骤&lt;/td&gt;&lt;td&gt;&lt;code&gt;setImmediate&lt;/code&gt;，没有渲染&lt;/td&gt;&lt;/tr&gt;&lt;/tbody&gt;&lt;/table&gt;&lt;p&gt;如果还用医院的类比：浏览器是一家带叫号屏的门诊，Node.js 则是医生&lt;strong&gt;按固定顺序轮流叫号&lt;/strong&gt;，先叫定时复诊的，再叫检查结果回来的，再叫插队登记的。&lt;/p&gt;&lt;/section&gt;&lt;section&gt;&lt;h3&gt;Python asyncio 的事件循环&lt;a href=&quot;#python-asyncio-的事件循环&quot;&gt;&lt;span&gt;#&lt;/span&gt;&lt;/a&gt;&lt;/h3&gt;&lt;p&gt;asyncio 和 Node.js 更像：负责”等”的也是操作系统。它通过标准库的 &lt;code&gt;selectors&lt;/code&gt; 模块使用 IO 多路复用，Linux 上是 epoll，macOS 上是 kqueue（Windows 上默认是另一种基于 IOCP 的实现）。&lt;/p&gt;&lt;p&gt;asyncio 的事件循环里有两样东西：&lt;/p&gt;&lt;ul&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;asyncio.sleep&lt;/code&gt; 登记的唤醒。&lt;/li&gt;
&lt;/ul&gt;&lt;p&gt;每一轮循环是这样的：&lt;/p&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;&lt;/span&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;&lt;/span&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;&lt;/span&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;&lt;/span&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;&lt;/span&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;&lt;p&gt;计算等待时长&lt;br /&gt;&lt;br /&gt;就绪队列不为空就是 0&lt;br /&gt;&lt;br /&gt;否则等到最近的定时器到期&lt;/p&gt;&lt;/span&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;&lt;p&gt;selector.select(等待时长)&lt;br /&gt;&lt;br /&gt;Linux 上就是 epoll_wait&lt;/p&gt;&lt;/span&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;&lt;p&gt;就绪 IO 对应的回调放入就绪队列&lt;/p&gt;&lt;/span&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;&lt;p&gt;到期定时器的回调放入就绪队列&lt;/p&gt;&lt;/span&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;&lt;p&gt;依次执行这一轮就绪队列里的回调&lt;/p&gt;&lt;/span&gt;&lt;/div&gt;
&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;&lt;/span&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;&lt;/span&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;&lt;/span&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;&lt;/span&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;&lt;/span&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;&lt;p&gt;计算等待时长&lt;br /&gt;&lt;br /&gt;就绪队列不为空就是 0&lt;br /&gt;&lt;br /&gt;否则等到最近的定时器到期&lt;/p&gt;&lt;/span&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;&lt;p&gt;selector.select(等待时长)&lt;br /&gt;&lt;br /&gt;Linux 上就是 epoll_wait&lt;/p&gt;&lt;/span&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;&lt;p&gt;就绪 IO 对应的回调放入就绪队列&lt;/p&gt;&lt;/span&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;&lt;p&gt;到期定时器的回调放入就绪队列&lt;/p&gt;&lt;/span&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;&lt;p&gt;依次执行这一轮就绪队列里的回调&lt;/p&gt;&lt;/span&gt;&lt;/div&gt;
&lt;/div&gt;&lt;/div&gt;&lt;/div&gt;&lt;p&gt;协程是怎么挂进这个循环的？&lt;code&gt;asyncio.run&lt;/code&gt; 或 &lt;code&gt;gather&lt;/code&gt; 会把协程包装成 &lt;strong&gt;Task&lt;/strong&gt;。Task 推进协程执行，直到遇到 &lt;code&gt;await&lt;/code&gt; 一个还没完成的操作，比如等数据库返回数据：&lt;/p&gt;&lt;ol&gt;
&lt;li&gt;底层把这个连接注册到 selector 上，表示”可读时通知我”；&lt;/li&gt;
&lt;li&gt;协程在 &lt;code&gt;await&lt;/code&gt; 处暂停，Task 把”恢复这个协程”登记成这个操作完成时的回调，然后把控制权还给事件循环；&lt;/li&gt;
&lt;li&gt;数据到达后，selector 返回这个连接，对应的回调读出数据、标记操作完成，再把”恢复协程”放进就绪队列；&lt;/li&gt;
&lt;li&gt;下一次轮到它时，协程从 &lt;code&gt;await&lt;/code&gt; 的下一行继续执行。&lt;/li&gt;
&lt;/ol&gt;&lt;p&gt;用一个小例子看执行顺序：&lt;/p&gt;&lt;div&gt;&lt;figure&gt;&lt;figcaption&gt;&lt;/figcaption&gt;&lt;pre&gt;&lt;code&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;1&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;import&lt;/span&gt;&lt;span&gt; asyncio&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;2&lt;/div&gt;&lt;/div&gt;&lt;div&gt;
&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;3&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;async&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;def&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;worker&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;name&lt;/span&gt;&lt;span&gt;,&lt;/span&gt;&lt;span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;delay&lt;/span&gt;&lt;/span&gt;&lt;span&gt;):&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;4&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;    &lt;/span&gt;&lt;span&gt;print&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;f&lt;/span&gt;&lt;span&gt;&quot;&lt;/span&gt;&lt;span&gt;{&lt;/span&gt;&lt;span&gt;name&lt;/span&gt;&lt;span&gt;}&lt;/span&gt;&lt;span&gt; 开始&quot;&lt;/span&gt;&lt;span&gt;)&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;5&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;    &lt;/span&gt;&lt;span&gt;await&lt;/span&gt;&lt;span&gt;&lt;span&gt; asyncio.&lt;/span&gt;&lt;span&gt;sleep&lt;/span&gt;&lt;span&gt;(delay)   &lt;/span&gt;&lt;/span&gt;&lt;span&gt;# 挂起：登记一个定时器，把控制权还给事件循环&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;6&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;    &lt;/span&gt;&lt;span&gt;print&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;f&lt;/span&gt;&lt;span&gt;&quot;&lt;/span&gt;&lt;span&gt;{&lt;/span&gt;&lt;span&gt;name&lt;/span&gt;&lt;span&gt;}&lt;/span&gt;&lt;span&gt; 结束&quot;&lt;/span&gt;&lt;span&gt;)&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;7&lt;/div&gt;&lt;/div&gt;&lt;div&gt;
&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;8&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;async&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;def&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;main&lt;/span&gt;&lt;span&gt;():&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;9&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;&lt;span&gt;    &lt;/span&gt;&lt;/span&gt;&lt;span&gt;loop &lt;/span&gt;&lt;span&gt;=&lt;/span&gt;&lt;span&gt; asyncio.&lt;/span&gt;&lt;span&gt;get_running_loop&lt;/span&gt;&lt;span&gt;()&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;10&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;&lt;span&gt;    &lt;/span&gt;&lt;/span&gt;&lt;span&gt;loop.&lt;/span&gt;&lt;span&gt;call_soon&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;lambda&lt;/span&gt;&lt;span&gt;: &lt;/span&gt;&lt;span&gt;print&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;&quot;call_soon 回调&quot;&lt;/span&gt;&lt;span&gt;))&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;11&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;    &lt;/span&gt;&lt;span&gt;await&lt;/span&gt;&lt;span&gt;&lt;span&gt; asyncio.&lt;/span&gt;&lt;span&gt;gather&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;worker&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;/span&gt;&lt;span&gt;&quot;A&quot;&lt;/span&gt;&lt;span&gt;, &lt;/span&gt;&lt;span&gt;0.2&lt;/span&gt;&lt;span&gt;&lt;span&gt;), &lt;/span&gt;&lt;span&gt;worker&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;/span&gt;&lt;span&gt;&quot;B&quot;&lt;/span&gt;&lt;span&gt;, &lt;/span&gt;&lt;span&gt;0.1&lt;/span&gt;&lt;span&gt;))&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;12&lt;/div&gt;&lt;/div&gt;&lt;div&gt;
&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;13&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;&lt;span&gt;asyncio.&lt;/span&gt;&lt;span&gt;run&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;main&lt;/span&gt;&lt;span&gt;())&lt;/span&gt;&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;/code&gt;&lt;/pre&gt;&lt;div&gt;&lt;div&gt;&lt;/div&gt;&lt;div&gt;&lt;/div&gt;&lt;/div&gt;&lt;/figure&gt;&lt;/div&gt;&lt;p&gt;输出：&lt;/p&gt;&lt;div&gt;&lt;figure&gt;&lt;figcaption&gt;&lt;/figcaption&gt;&lt;pre&gt;&lt;code&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;1&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;call_soon 回调&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;2&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;A 开始&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;3&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;B 开始&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;4&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;B 结束&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;5&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;A 结束&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;/code&gt;&lt;/pre&gt;&lt;div&gt;&lt;div&gt;&lt;/div&gt;&lt;div&gt;&lt;/div&gt;&lt;/div&gt;&lt;/figure&gt;&lt;/div&gt;&lt;p&gt;&lt;code&gt;call_soon&lt;/code&gt; 的回调先进了就绪队列，&lt;code&gt;gather&lt;/code&gt; 创建的两个 Task 排在它后面，所以按顺序先打印”call_soon 回调”，再分别开始 A 和 B。两个协程遇到 &lt;code&gt;sleep&lt;/code&gt; 都挂起了，事件循环在 epoll 上等到最近的定时器（0.1 秒）到期，先恢复 B，再等到 0.2 秒恢复 A。&lt;/p&gt;&lt;p&gt;和 JavaScript 比，asyncio 的规则更简单：&lt;strong&gt;没有宏任务和微任务之分&lt;/strong&gt;，所有马上要执行的回调都在同一个就绪队列里排队。医院的类比里，就是医生每一轮先看看下一个复诊病人还要多久，最多等那么久；检验科的结果和到点的复诊一起放进候诊区，医生再按顺序一个个看。&lt;/p&gt;&lt;/section&gt;&lt;section&gt;&lt;h3&gt;三者放在一起&lt;a href=&quot;#三者放在一起&quot;&gt;&lt;span&gt;#&lt;/span&gt;&lt;/a&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;Node.js&lt;/th&gt;&lt;th&gt;Python asyncio&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;epoll + 文件 IO 线程池&lt;/td&gt;&lt;td&gt;epoll（或 kqueue 等）&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;队列&lt;/td&gt;&lt;td&gt;宏任务、微任务&lt;/td&gt;&lt;td&gt;分阶段的队列、微任务、nextTick&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;code&gt;await&lt;/code&gt;&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;长时间计算的后果&lt;/td&gt;&lt;td&gt;页面卡住&lt;/td&gt;&lt;td&gt;整个服务卡住&lt;/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;。它们解决的是”等”的问题，不是”算”的问题。这也是我在百度最后用了多进程 + 协程的原因：协程负责让每个进程在等 Palo 的时候不闲着，而单个事件循环只能用满一个核的上限，要靠多开几个”诊室”（进程）来突破。&lt;/p&gt;&lt;p&gt;所以在百度遇到这个问题时，与其说是学了一个新技术，不如说是把以前在前端想明白的东西搬到了后端：&lt;strong&gt;让医生一直在看病人，而不是在门口等结果。&lt;/strong&gt;&lt;/p&gt;&lt;/section&gt;&lt;/section&gt;</content:encoded></item><item><title>CDN 新客户接入前的价格测算：用波形拟合算出最低成本价</title><link>https://tracerzzz.cn/posts/cdn-new-customer-pricing/</link><guid isPermaLink="true">https://tracerzzz.cn/posts/cdn-new-customer-pricing/</guid><description>火山 CDN 经营分析平台上的价格测算功能：销售拿到一个新客户的带宽打点，平台把它和大盘拟合，判断客户是否错峰、算出边际超卖倍数，再结合资源诉求的加权成本和包头系数，给出不同计费方式下的最低成本价。这篇讲它的输入、计算过程、示例和后续的优化方向。</description><pubDate>Sun, 25 Oct 2026 13:00:00 GMT</pubDate><content:encoded>&lt;p&gt;接入一个新的 CDN 客户之前，销售最常问的问题是：&lt;strong&gt;这个客户最低能报多少钱？&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;报高了，客户被竞争对手抢走；报低了，接进来就亏钱。而 CDN 的成本又很特殊：同样大小的两个客户，因为高峰时间不同，给厂商带来的成本可能差好几倍（见&lt;a href=&quot;/posts/cdn-off-peak-and-overselling/&quot;&gt;《CDN 的错峰与超卖》&lt;/a&gt;）。光靠经验拍价格，很难准。&lt;/p&gt;
&lt;p&gt;火山 CDN 的经营分析平台上有一个&lt;strong&gt;价格测算&lt;/strong&gt;功能，就是用来回答这个问题的。这篇介绍它的思路。&lt;/p&gt;
&lt;blockquote&gt;&lt;p&gt;文中的数据都是模拟的，成本用相对指数表示，不代表真实价格。&lt;/p&gt;&lt;/blockquote&gt;
&lt;section&gt;&lt;h2&gt;一、输入&lt;a href=&quot;#一输入&quot;&gt;&lt;span&gt;#&lt;/span&gt;&lt;/a&gt;&lt;/h2&gt;&lt;p&gt;使用者（一般是销售或运营）需要提供三类输入：&lt;/p&gt;&lt;p&gt;&lt;strong&gt;1. 客户的带宽打点。&lt;/strong&gt; 5 分钟粒度的带宽数据，要求：&lt;/p&gt;&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;至少 7 天&lt;/strong&gt;：覆盖一个完整的周周期，把工作日和周末的差异都包含进去；&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;最能体现客户业务特性的稳定波形&lt;/strong&gt;：不能拿一个做活动的特殊时段来测，否则拟合出来的结果不代表客户的常态。&lt;/li&gt;
&lt;/ul&gt;&lt;p&gt;这两点是测算结果可信的前提，平台对使用者专门做过宣贯。错峰效果完全取决于客户的波形，输入的波形不对，后面算得再精确也没有意义。&lt;/p&gt;&lt;p&gt;&lt;strong&gt;2. 资源诉求。&lt;/strong&gt; 客户对接入资源的要求，比如全部要联通、全部要电信，或者可以接受非 80 端口的低价节点、PCDN 节点等。不同资源的成本差别很大。&lt;/p&gt;&lt;p&gt;&lt;strong&gt;3. 包头系数。&lt;/strong&gt; 后面会讲到，它是留给销售的议价空间，测算时可以调整。&lt;/p&gt;&lt;/section&gt;
&lt;section&gt;&lt;h2&gt;二、测算过程&lt;a href=&quot;#二测算过程&quot;&gt;&lt;span&gt;#&lt;/span&gt;&lt;/a&gt;&lt;/h2&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;&lt;/span&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;&lt;/span&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;&lt;/span&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;&lt;/span&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;&lt;/span&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;&lt;/span&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;&lt;/span&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;&lt;/span&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;&lt;/span&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;&lt;/span&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;&lt;/span&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;&lt;/span&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;&lt;/span&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;&lt;p&gt;客户打点（≥ 7 天）&lt;/p&gt;&lt;/span&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;&lt;p&gt;与大盘波形拟合，得到新大盘&lt;/p&gt;&lt;/span&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;&lt;p&gt;大盘历史打点&lt;/p&gt;&lt;/span&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;&lt;p&gt;大盘 95 增量 ΔB&lt;/p&gt;&lt;/span&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;&lt;p&gt;客户自身的计费用量&lt;br /&gt;&lt;br /&gt;95 / 日峰月均 / 流量……&lt;/p&gt;&lt;/span&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;&lt;p&gt;错峰带宽 = 客户 95 − ΔB&lt;br /&gt;&lt;br /&gt;是否为正？&lt;/p&gt;&lt;/span&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;&lt;p&gt;边际超卖倍数 k&lt;/p&gt;&lt;/span&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;&lt;p&gt;资源诉求&lt;/p&gt;&lt;/span&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;&lt;p&gt;加权资源成本 C&lt;/p&gt;&lt;/span&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;&lt;p&gt;增量成本 = ΔB × C&lt;/p&gt;&lt;/span&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;&lt;p&gt;各计费方式的最低成本价&lt;/p&gt;&lt;/span&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;&lt;p&gt;包头系数 h&lt;/p&gt;&lt;/span&gt;&lt;/div&gt;
&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;&lt;/span&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;&lt;/span&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;&lt;/span&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;&lt;/span&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;&lt;/span&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;&lt;/span&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;&lt;/span&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;&lt;/span&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;&lt;/span&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;&lt;/span&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;&lt;/span&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;&lt;/span&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;&lt;/span&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;&lt;p&gt;客户打点（≥ 7 天）&lt;/p&gt;&lt;/span&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;&lt;p&gt;与大盘波形拟合，得到新大盘&lt;/p&gt;&lt;/span&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;&lt;p&gt;大盘历史打点&lt;/p&gt;&lt;/span&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;&lt;p&gt;大盘 95 增量 ΔB&lt;/p&gt;&lt;/span&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;&lt;p&gt;客户自身的计费用量&lt;br /&gt;&lt;br /&gt;95 / 日峰月均 / 流量……&lt;/p&gt;&lt;/span&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;&lt;p&gt;错峰带宽 = 客户 95 − ΔB&lt;br /&gt;&lt;br /&gt;是否为正？&lt;/p&gt;&lt;/span&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;&lt;p&gt;边际超卖倍数 k&lt;/p&gt;&lt;/span&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;&lt;p&gt;资源诉求&lt;/p&gt;&lt;/span&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;&lt;p&gt;加权资源成本 C&lt;/p&gt;&lt;/span&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;&lt;p&gt;增量成本 = ΔB × C&lt;/p&gt;&lt;/span&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;&lt;p&gt;各计费方式的最低成本价&lt;/p&gt;&lt;/span&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;&lt;p&gt;包头系数 h&lt;/p&gt;&lt;/span&gt;&lt;/div&gt;
&lt;/div&gt;&lt;/div&gt;&lt;/div&gt;&lt;section&gt;&lt;h3&gt;1. 波形拟合：算出大盘 95 的增量&lt;a href=&quot;#1-波形拟合算出大盘-95-的增量&quot;&gt;&lt;span&gt;#&lt;/span&gt;&lt;/a&gt;&lt;/h3&gt;&lt;p&gt;把客户的打点和大盘同一时段的打点逐点相加，得到「接入后的新大盘」，再分别计算新、旧大盘的 95，两者之差就是这个客户带来的&lt;strong&gt;大盘 95 增量 ΔB&lt;/strong&gt;。&lt;/p&gt;&lt;/section&gt;&lt;section&gt;&lt;h3&gt;2. 判断错峰&lt;a href=&quot;#2-判断错峰&quot;&gt;&lt;span&gt;#&lt;/span&gt;&lt;/a&gt;&lt;/h3&gt;&lt;span&gt;&lt;span&gt;&lt;span&gt;错峰带宽=B客户−ΔB,k=B客户ΔB\text{错峰带宽} = B_{\text{客户}} - \Delta B, \qquad k = \frac{B_{\text{客户}}}{\Delta B}&lt;/span&gt;&lt;span&gt;&lt;span&gt;&lt;span&gt;&lt;/span&gt;&lt;span&gt;&lt;span&gt;错峰带宽&lt;/span&gt;&lt;/span&gt;&lt;span&gt;&lt;/span&gt;&lt;span&gt;=&lt;/span&gt;&lt;span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span&gt;&lt;span&gt;&lt;/span&gt;&lt;span&gt;&lt;span&gt;B&lt;/span&gt;&lt;span&gt;&lt;span&gt;&lt;span&gt;&lt;span&gt;&lt;span&gt;&lt;span&gt;&lt;/span&gt;&lt;span&gt;&lt;span&gt;&lt;span&gt;&lt;span&gt;客户&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span&gt;​&lt;/span&gt;&lt;/span&gt;&lt;span&gt;&lt;span&gt;&lt;span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span&gt;&lt;/span&gt;&lt;span&gt;−&lt;/span&gt;&lt;span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span&gt;&lt;span&gt;&lt;/span&gt;&lt;span&gt;Δ&lt;/span&gt;&lt;span&gt;B&lt;/span&gt;&lt;span&gt;,&lt;/span&gt;&lt;span&gt;&lt;/span&gt;&lt;span&gt;&lt;/span&gt;&lt;span&gt;k&lt;/span&gt;&lt;span&gt;&lt;/span&gt;&lt;span&gt;=&lt;/span&gt;&lt;span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span&gt;&lt;span&gt;&lt;/span&gt;&lt;span&gt;&lt;span&gt;&lt;/span&gt;&lt;span&gt;&lt;span&gt;&lt;span&gt;&lt;span&gt;&lt;span&gt;&lt;span&gt;&lt;/span&gt;&lt;span&gt;&lt;span&gt;Δ&lt;/span&gt;&lt;span&gt;B&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span&gt;&lt;span&gt;&lt;/span&gt;&lt;span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span&gt;&lt;span&gt;&lt;/span&gt;&lt;span&gt;&lt;span&gt;&lt;span&gt;B&lt;/span&gt;&lt;span&gt;&lt;span&gt;&lt;span&gt;&lt;span&gt;&lt;span&gt;&lt;span&gt;&lt;/span&gt;&lt;span&gt;&lt;span&gt;&lt;span&gt;&lt;span&gt;客户&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span&gt;​&lt;/span&gt;&lt;/span&gt;&lt;span&gt;&lt;span&gt;&lt;span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span&gt;​&lt;/span&gt;&lt;/span&gt;&lt;span&gt;&lt;span&gt;&lt;span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;p&gt;B_客户 是客户自己的 95。错峰带宽为正（k &amp;gt; 1），说明客户的高峰落在大盘的低谷，是错峰客户；为零或为负，说明和大盘同峰。&lt;/p&gt;&lt;p&gt;&lt;strong&gt;这是测算里最重要的结论。&lt;/strong&gt; 价格是参考，错峰的正负才是判断这个客户「好不好」的依据。&lt;/p&gt;&lt;/section&gt;&lt;section&gt;&lt;h3&gt;3. 加权资源成本&lt;a href=&quot;#3-加权资源成本&quot;&gt;&lt;span&gt;#&lt;/span&gt;&lt;/a&gt;&lt;/h3&gt;&lt;p&gt;客户的资源诉求决定了成本单价。把各类资源的成本按客户的流量占比加权平均：&lt;/p&gt;&lt;span&gt;&lt;span&gt;&lt;span&gt;C=∑iwi⋅ciC = \sum_i w_i \cdot c_i&lt;/span&gt;&lt;span&gt;&lt;span&gt;&lt;span&gt;&lt;/span&gt;&lt;span&gt;C&lt;/span&gt;&lt;span&gt;&lt;/span&gt;&lt;span&gt;=&lt;/span&gt;&lt;span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span&gt;&lt;span&gt;&lt;/span&gt;&lt;span&gt;&lt;span&gt;&lt;span&gt;&lt;span&gt;&lt;span&gt;&lt;span&gt;&lt;/span&gt;&lt;span&gt;&lt;span&gt;i&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span&gt;&lt;span&gt;&lt;/span&gt;&lt;span&gt;&lt;span&gt;∑&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span&gt;​&lt;/span&gt;&lt;/span&gt;&lt;span&gt;&lt;span&gt;&lt;span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span&gt;&lt;/span&gt;&lt;span&gt;&lt;span&gt;w&lt;/span&gt;&lt;span&gt;&lt;span&gt;&lt;span&gt;&lt;span&gt;&lt;span&gt;&lt;span&gt;&lt;/span&gt;&lt;span&gt;&lt;span&gt;i&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span&gt;​&lt;/span&gt;&lt;/span&gt;&lt;span&gt;&lt;span&gt;&lt;span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span&gt;&lt;/span&gt;&lt;span&gt;⋅&lt;/span&gt;&lt;span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span&gt;&lt;span&gt;&lt;/span&gt;&lt;span&gt;&lt;span&gt;c&lt;/span&gt;&lt;span&gt;&lt;span&gt;&lt;span&gt;&lt;span&gt;&lt;span&gt;&lt;span&gt;&lt;/span&gt;&lt;span&gt;&lt;span&gt;i&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span&gt;​&lt;/span&gt;&lt;/span&gt;&lt;span&gt;&lt;span&gt;&lt;span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&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;占比 wᵢ&lt;/th&gt;&lt;th&gt;成本指数 cᵢ&lt;/th&gt;&lt;/tr&gt;&lt;/thead&gt;&lt;tbody&gt;&lt;tr&gt;&lt;td&gt;联通&lt;/td&gt;&lt;td&gt;50%&lt;/td&gt;&lt;td&gt;1.0&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;电信&lt;/td&gt;&lt;td&gt;30%&lt;/td&gt;&lt;td&gt;1.2&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;非 80 端口节点&lt;/td&gt;&lt;td&gt;20%&lt;/td&gt;&lt;td&gt;0.6&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;&lt;strong&gt;加权成本 C&lt;/strong&gt;&lt;/td&gt;&lt;td&gt;&lt;/td&gt;&lt;td&gt;&lt;strong&gt;0.98&lt;/strong&gt;&lt;/td&gt;&lt;/tr&gt;&lt;/tbody&gt;&lt;/table&gt;&lt;p&gt;这里的加权，本来就是为了把分运营商、分资源的细分成本价模糊成一个数，和「一个大盘」的拟合方式相匹配。&lt;/p&gt;&lt;/section&gt;&lt;section&gt;&lt;h3&gt;4. 最低成本价&lt;a href=&quot;#4-最低成本价&quot;&gt;&lt;span&gt;#&lt;/span&gt;&lt;/a&gt;&lt;/h3&gt;&lt;p&gt;接入这个客户，厂商要多付的成本是 &lt;strong&gt;ΔB × C&lt;/strong&gt;。向客户收费时，计费用量要乘以&lt;strong&gt;包头系数 h&lt;/strong&gt;。最低成本价的原则是：&lt;strong&gt;乘上包头系数之后的收入，不能低于增加的成本&lt;/strong&gt;。&lt;/p&gt;&lt;span&gt;&lt;span&gt;&lt;span&gt;Pmin⁡×h×B客户 ≥ ΔB×C⟹Pmin⁡=Ck×hP_{\min} \times h \times B_{\text{客户}} \ \ge\ \Delta B \times C
\quad\Longrightarrow\quad
P_{\min} = \frac{C}{k \times h}&lt;/span&gt;&lt;span&gt;&lt;span&gt;&lt;span&gt;&lt;/span&gt;&lt;span&gt;&lt;span&gt;P&lt;/span&gt;&lt;span&gt;&lt;span&gt;&lt;span&gt;&lt;span&gt;&lt;span&gt;&lt;span&gt;&lt;/span&gt;&lt;span&gt;&lt;span&gt;&lt;span&gt;&lt;span&gt;m&lt;/span&gt;&lt;span&gt;i&lt;/span&gt;&lt;span&gt;n&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span&gt;​&lt;/span&gt;&lt;/span&gt;&lt;span&gt;&lt;span&gt;&lt;span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span&gt;&lt;/span&gt;&lt;span&gt;×&lt;/span&gt;&lt;span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span&gt;&lt;span&gt;&lt;/span&gt;&lt;span&gt;h&lt;/span&gt;&lt;span&gt;&lt;/span&gt;&lt;span&gt;×&lt;/span&gt;&lt;span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span&gt;&lt;span&gt;&lt;/span&gt;&lt;span&gt;&lt;span&gt;B&lt;/span&gt;&lt;span&gt;&lt;span&gt;&lt;span&gt;&lt;span&gt;&lt;span&gt;&lt;span&gt;&lt;/span&gt;&lt;span&gt;&lt;span&gt;&lt;span&gt;&lt;span&gt;客户&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span&gt;​&lt;/span&gt;&lt;/span&gt;&lt;span&gt;&lt;span&gt;&lt;span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;&lt;/span&gt;&lt;span&gt;≥&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span&gt;&lt;span&gt;&lt;/span&gt;&lt;span&gt;Δ&lt;/span&gt;&lt;span&gt;B&lt;/span&gt;&lt;span&gt;&lt;/span&gt;&lt;span&gt;×&lt;/span&gt;&lt;span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span&gt;&lt;span&gt;&lt;/span&gt;&lt;span&gt;C&lt;/span&gt;&lt;span&gt;&lt;/span&gt;&lt;span&gt;&lt;/span&gt;&lt;span&gt;⟹&lt;/span&gt;&lt;span&gt;&lt;/span&gt;&lt;span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span&gt;&lt;span&gt;&lt;/span&gt;&lt;span&gt;&lt;span&gt;P&lt;/span&gt;&lt;span&gt;&lt;span&gt;&lt;span&gt;&lt;span&gt;&lt;span&gt;&lt;span&gt;&lt;/span&gt;&lt;span&gt;&lt;span&gt;&lt;span&gt;&lt;span&gt;m&lt;/span&gt;&lt;span&gt;i&lt;/span&gt;&lt;span&gt;n&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span&gt;​&lt;/span&gt;&lt;/span&gt;&lt;span&gt;&lt;span&gt;&lt;span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span&gt;&lt;/span&gt;&lt;span&gt;=&lt;/span&gt;&lt;span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span&gt;&lt;span&gt;&lt;/span&gt;&lt;span&gt;&lt;span&gt;&lt;/span&gt;&lt;span&gt;&lt;span&gt;&lt;span&gt;&lt;span&gt;&lt;span&gt;&lt;span&gt;&lt;/span&gt;&lt;span&gt;&lt;span&gt;k&lt;/span&gt;&lt;span&gt;&lt;/span&gt;&lt;span&gt;×&lt;/span&gt;&lt;span&gt;&lt;/span&gt;&lt;span&gt;h&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span&gt;&lt;span&gt;&lt;/span&gt;&lt;span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span&gt;&lt;span&gt;&lt;/span&gt;&lt;span&gt;&lt;span&gt;C&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span&gt;​&lt;/span&gt;&lt;/span&gt;&lt;span&gt;&lt;span&gt;&lt;span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;p&gt;这个式子很好地说明了各个因素的作用：&lt;/p&gt;&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;k 越大（越错峰）&lt;/strong&gt;，最低价越低；&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;资源越贵（C 越大）&lt;/strong&gt;，最低价越高；&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;包头系数 h 越大&lt;/strong&gt;，同样的单价能收回更多，报价就可以更低。h 在测算时是一个可调的输入，用来给销售留出议价空间。&lt;/li&gt;
&lt;/ul&gt;&lt;/section&gt;&lt;section&gt;&lt;h3&gt;5. 不同计费方式的最低价&lt;a href=&quot;#5-不同计费方式的最低价&quot;&gt;&lt;span&gt;#&lt;/span&gt;&lt;/a&gt;&lt;/h3&gt;&lt;p&gt;客户可能希望按 95、按日峰月均或者按流量计费。成本侧的增量都是同一个 ΔB × C，不同的只是客户在各计费方式下的计费用量 Q。平台会根据上传的打点，分别算出各计费方式的用量，再分别给出最低价：&lt;/p&gt;&lt;span&gt;&lt;span&gt;&lt;span&gt;Pmin⁡, m=ΔB×CQm×hP_{\min,\,m} = \frac{\Delta B \times C}{Q_m \times h}&lt;/span&gt;&lt;span&gt;&lt;span&gt;&lt;span&gt;&lt;/span&gt;&lt;span&gt;&lt;span&gt;P&lt;/span&gt;&lt;span&gt;&lt;span&gt;&lt;span&gt;&lt;span&gt;&lt;span&gt;&lt;span&gt;&lt;/span&gt;&lt;span&gt;&lt;span&gt;&lt;span&gt;&lt;span&gt;m&lt;/span&gt;&lt;span&gt;i&lt;/span&gt;&lt;span&gt;n&lt;/span&gt;&lt;/span&gt;&lt;span&gt;,&lt;/span&gt;&lt;span&gt;&lt;/span&gt;&lt;span&gt;m&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span&gt;​&lt;/span&gt;&lt;/span&gt;&lt;span&gt;&lt;span&gt;&lt;span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span&gt;&lt;/span&gt;&lt;span&gt;=&lt;/span&gt;&lt;span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span&gt;&lt;span&gt;&lt;/span&gt;&lt;span&gt;&lt;span&gt;&lt;/span&gt;&lt;span&gt;&lt;span&gt;&lt;span&gt;&lt;span&gt;&lt;span&gt;&lt;span&gt;&lt;/span&gt;&lt;span&gt;&lt;span&gt;&lt;span&gt;Q&lt;/span&gt;&lt;span&gt;&lt;span&gt;&lt;span&gt;&lt;span&gt;&lt;span&gt;&lt;span&gt;&lt;/span&gt;&lt;span&gt;&lt;span&gt;m&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span&gt;​&lt;/span&gt;&lt;/span&gt;&lt;span&gt;&lt;span&gt;&lt;span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span&gt;&lt;/span&gt;&lt;span&gt;×&lt;/span&gt;&lt;span&gt;&lt;/span&gt;&lt;span&gt;h&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span&gt;&lt;span&gt;&lt;/span&gt;&lt;span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span&gt;&lt;span&gt;&lt;/span&gt;&lt;span&gt;&lt;span&gt;Δ&lt;/span&gt;&lt;span&gt;B&lt;/span&gt;&lt;span&gt;&lt;/span&gt;&lt;span&gt;×&lt;/span&gt;&lt;span&gt;&lt;/span&gt;&lt;span&gt;C&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span&gt;​&lt;/span&gt;&lt;/span&gt;&lt;span&gt;&lt;span&gt;&lt;span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;p&gt;m 代表计费方式。销售可以同时看到「按 95 最低报多少、按流量最低报多少」，按客户的偏好去谈。&lt;/p&gt;&lt;/section&gt;&lt;/section&gt;
&lt;section&gt;&lt;h2&gt;三、示例&lt;a href=&quot;#三示例&quot;&gt;&lt;span&gt;#&lt;/span&gt;&lt;/a&gt;&lt;/h2&gt;&lt;p&gt;用 7 天的模拟数据，测算两个规模相近的客户：客户甲的高峰在上午 10 点，客户乙的高峰在晚上 21 点，和大盘一样。大盘的 95 约为 679 Mbps，资源诉求都按上面的表格（C = 0.98）。&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;客户自己的 95&lt;/th&gt;&lt;th&gt;大盘 95 增量 ΔB&lt;/th&gt;&lt;th&gt;错峰带宽&lt;/th&gt;&lt;th&gt;边际超卖倍数 k&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;208.5&lt;/td&gt;&lt;td&gt;22.1&lt;/td&gt;&lt;td&gt;&lt;strong&gt;+186.4&lt;/strong&gt;&lt;/td&gt;&lt;td&gt;9.42&lt;/td&gt;&lt;td&gt;错峰&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;客户乙&lt;/td&gt;&lt;td&gt;207.0&lt;/td&gt;&lt;td&gt;208.8&lt;/td&gt;&lt;td&gt;&lt;strong&gt;−1.9&lt;/strong&gt;&lt;/td&gt;&lt;td&gt;0.99&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;（成本指数；95 和日峰月均为每 Mbps·月，流量为每 TB）：&lt;/p&gt;

&lt;table&gt;&lt;thead&gt;&lt;tr&gt;&lt;th&gt;计费方式&lt;/th&gt;&lt;th&gt;客户甲（h = 1.0）&lt;/th&gt;&lt;th&gt;客户甲（h = 1.1）&lt;/th&gt;&lt;th&gt;客户乙（h = 1.0）&lt;/th&gt;&lt;th&gt;客户乙（h = 1.1）&lt;/th&gt;&lt;/tr&gt;&lt;/thead&gt;&lt;tbody&gt;&lt;tr&gt;&lt;td&gt;95 峰值&lt;/td&gt;&lt;td&gt;0.104&lt;/td&gt;&lt;td&gt;0.095&lt;/td&gt;&lt;td&gt;0.989&lt;/td&gt;&lt;td&gt;0.899&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;日峰月均&lt;/td&gt;&lt;td&gt;0.096&lt;/td&gt;&lt;td&gt;0.087&lt;/td&gt;&lt;td&gt;0.896&lt;/td&gt;&lt;td&gt;0.815&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;流量&lt;/td&gt;&lt;td&gt;1.18&lt;/td&gt;&lt;td&gt;1.07&lt;/td&gt;&lt;td&gt;11.46&lt;/td&gt;&lt;td&gt;10.42&lt;/td&gt;&lt;/tr&gt;&lt;/tbody&gt;&lt;/table&gt;&lt;p&gt;两个客户规模几乎一样，资源诉求也一样，但客户甲的最低成本价只有客户乙的十分之一左右。如果按经验给两个客户报同一个价格，要么错失客户甲，要么接入客户乙就亏钱。&lt;/p&gt;&lt;/section&gt;
&lt;section&gt;&lt;h2&gt;四、回头看：可以继续优化的地方&lt;a href=&quot;#四回头看可以继续优化的地方&quot;&gt;&lt;span&gt;#&lt;/span&gt;&lt;/a&gt;&lt;/h2&gt;&lt;p&gt;先看优化后的测算流程，可以和第二节的流程图对照。虚线框是新增或改变的步骤：&lt;/p&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;&lt;/span&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;&lt;/span&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;&lt;/span&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;&lt;/span&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;&lt;/span&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;&lt;/span&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;&lt;/span&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;&lt;/span&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;&lt;/span&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;&lt;/span&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;&lt;/span&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;&lt;/span&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;&lt;/span&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;&lt;/span&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;&lt;/span&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;&lt;/span&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;&lt;/span&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;&lt;/span&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;&lt;/span&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;&lt;p&gt;客户打点（≥ 7 天）&lt;/p&gt;&lt;/span&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;&lt;p&gt;按资源占比拆分客户波形&lt;/p&gt;&lt;/span&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;&lt;p&gt;资源诉求（各资源占比）&lt;/p&gt;&lt;/span&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;&lt;p&gt;联通部分&lt;/p&gt;&lt;/span&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;&lt;p&gt;电信部分&lt;/p&gt;&lt;/span&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;&lt;p&gt;非 80 / PCDN 部分&lt;/p&gt;&lt;/span&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;&lt;p&gt;与联通大盘拟合&lt;/p&gt;&lt;/span&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;&lt;p&gt;与电信大盘拟合&lt;/p&gt;&lt;/span&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;&lt;p&gt;与非 80 / PCDN 大盘拟合&lt;/p&gt;&lt;/span&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;&lt;p&gt;ΔB₁，错峰判断&lt;/p&gt;&lt;/span&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;&lt;p&gt;ΔB₂，错峰判断&lt;/p&gt;&lt;/span&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;&lt;p&gt;ΔB₃，错峰判断&lt;/p&gt;&lt;/span&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;&lt;p&gt;增量保护：ΔBᵢ 不低于 Bᵢ ÷ k_max&lt;/p&gt;&lt;/span&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;&lt;p&gt;增量成本 = Σ ΔBᵢ × 各池成本价 cᵢ + 固定成本分摊&lt;/p&gt;&lt;/span&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;&lt;p&gt;客户自身的计费用量&lt;br /&gt;&lt;br /&gt;95 / 日峰月均 / 流量……&lt;/p&gt;&lt;/span&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;&lt;p&gt;各计费方式的最低成本价&lt;/p&gt;&lt;/span&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;&lt;p&gt;包头系数 h&lt;/p&gt;&lt;/span&gt;&lt;/div&gt;
&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;&lt;/span&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;&lt;/span&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;&lt;/span&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;&lt;/span&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;&lt;/span&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;&lt;/span&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;&lt;/span&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;&lt;/span&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;&lt;/span&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;&lt;/span&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;&lt;/span&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;&lt;/span&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;&lt;/span&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;&lt;/span&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;&lt;/span&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;&lt;/span&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;&lt;/span&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;&lt;/span&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;&lt;/span&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;&lt;p&gt;客户打点（≥ 7 天）&lt;/p&gt;&lt;/span&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;&lt;p&gt;按资源占比拆分客户波形&lt;/p&gt;&lt;/span&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;&lt;p&gt;资源诉求（各资源占比）&lt;/p&gt;&lt;/span&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;&lt;p&gt;联通部分&lt;/p&gt;&lt;/span&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;&lt;p&gt;电信部分&lt;/p&gt;&lt;/span&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;&lt;p&gt;非 80 / PCDN 部分&lt;/p&gt;&lt;/span&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;&lt;p&gt;与联通大盘拟合&lt;/p&gt;&lt;/span&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;&lt;p&gt;与电信大盘拟合&lt;/p&gt;&lt;/span&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;&lt;p&gt;与非 80 / PCDN 大盘拟合&lt;/p&gt;&lt;/span&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;&lt;p&gt;ΔB₁，错峰判断&lt;/p&gt;&lt;/span&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;&lt;p&gt;ΔB₂，错峰判断&lt;/p&gt;&lt;/span&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;&lt;p&gt;ΔB₃，错峰判断&lt;/p&gt;&lt;/span&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;&lt;p&gt;增量保护：ΔBᵢ 不低于 Bᵢ ÷ k_max&lt;/p&gt;&lt;/span&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;&lt;p&gt;增量成本 = Σ ΔBᵢ × 各池成本价 cᵢ + 固定成本分摊&lt;/p&gt;&lt;/span&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;&lt;p&gt;客户自身的计费用量&lt;br /&gt;&lt;br /&gt;95 / 日峰月均 / 流量……&lt;/p&gt;&lt;/span&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;&lt;p&gt;各计费方式的最低成本价&lt;/p&gt;&lt;/span&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;&lt;p&gt;包头系数 h&lt;/p&gt;&lt;/span&gt;&lt;/div&gt;
&lt;/div&gt;&lt;/div&gt;&lt;/div&gt;&lt;p&gt;和原来相比，主要有两处变化：一个大盘变成了按资源池划分的多个大盘；算出增量之后、计算成本之前，多了一道保护。下面分别说。&lt;/p&gt;&lt;p&gt;&lt;strong&gt;1. 分资源池拟合。&lt;/strong&gt; 当时是把所有资源放在一个大盘里拟合，再用加权成本。但实际上，上游往往是按运营商、按资源池分别结算 95 的，联通的大盘和电信的大盘高峰时间也未必一样。更精确的做法是&lt;strong&gt;把每类资源当成一个独立的大盘&lt;/strong&gt;：按客户的资源占比，把客户的波形拆到各个大盘上，分别拟合、分别算增量，每个大盘用自己的成本价：&lt;/p&gt;&lt;span&gt;&lt;span&gt;&lt;span&gt;Pmin⁡=∑iΔBi⋅cih×B客户P_{\min} = \frac{\sum_i \Delta B_i \cdot c_i}{h \times B_{\text{客户}}}&lt;/span&gt;&lt;span&gt;&lt;span&gt;&lt;span&gt;&lt;/span&gt;&lt;span&gt;&lt;span&gt;P&lt;/span&gt;&lt;span&gt;&lt;span&gt;&lt;span&gt;&lt;span&gt;&lt;span&gt;&lt;span&gt;&lt;/span&gt;&lt;span&gt;&lt;span&gt;&lt;span&gt;&lt;span&gt;m&lt;/span&gt;&lt;span&gt;i&lt;/span&gt;&lt;span&gt;n&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span&gt;​&lt;/span&gt;&lt;/span&gt;&lt;span&gt;&lt;span&gt;&lt;span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span&gt;&lt;/span&gt;&lt;span&gt;=&lt;/span&gt;&lt;span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span&gt;&lt;span&gt;&lt;/span&gt;&lt;span&gt;&lt;span&gt;&lt;/span&gt;&lt;span&gt;&lt;span&gt;&lt;span&gt;&lt;span&gt;&lt;span&gt;&lt;span&gt;&lt;/span&gt;&lt;span&gt;&lt;span&gt;h&lt;/span&gt;&lt;span&gt;&lt;/span&gt;&lt;span&gt;×&lt;/span&gt;&lt;span&gt;&lt;/span&gt;&lt;span&gt;&lt;span&gt;B&lt;/span&gt;&lt;span&gt;&lt;span&gt;&lt;span&gt;&lt;span&gt;&lt;span&gt;&lt;span&gt;&lt;/span&gt;&lt;span&gt;&lt;span&gt;&lt;span&gt;&lt;span&gt;客户&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span&gt;​&lt;/span&gt;&lt;/span&gt;&lt;span&gt;&lt;span&gt;&lt;span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span&gt;&lt;span&gt;&lt;/span&gt;&lt;span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span&gt;&lt;span&gt;&lt;/span&gt;&lt;span&gt;&lt;span&gt;&lt;span&gt;∑&lt;/span&gt;&lt;span&gt;&lt;span&gt;&lt;span&gt;&lt;span&gt;&lt;span&gt;&lt;span&gt;&lt;/span&gt;&lt;span&gt;&lt;span&gt;i&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span&gt;​&lt;/span&gt;&lt;/span&gt;&lt;span&gt;&lt;span&gt;&lt;span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span&gt;&lt;/span&gt;&lt;span&gt;Δ&lt;/span&gt;&lt;span&gt;&lt;span&gt;B&lt;/span&gt;&lt;span&gt;&lt;span&gt;&lt;span&gt;&lt;span&gt;&lt;span&gt;&lt;span&gt;&lt;/span&gt;&lt;span&gt;&lt;span&gt;i&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span&gt;​&lt;/span&gt;&lt;/span&gt;&lt;span&gt;&lt;span&gt;&lt;span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span&gt;&lt;/span&gt;&lt;span&gt;⋅&lt;/span&gt;&lt;span&gt;&lt;/span&gt;&lt;span&gt;&lt;span&gt;c&lt;/span&gt;&lt;span&gt;&lt;span&gt;&lt;span&gt;&lt;span&gt;&lt;span&gt;&lt;span&gt;&lt;/span&gt;&lt;span&gt;&lt;span&gt;i&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span&gt;​&lt;/span&gt;&lt;/span&gt;&lt;span&gt;&lt;span&gt;&lt;span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span&gt;​&lt;/span&gt;&lt;/span&gt;&lt;span&gt;&lt;span&gt;&lt;span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;p&gt;这时资源占比就不再用于加权成本，而是用于拆分客户的波形；每个资源池也有各自的错峰情况。&lt;/p&gt;&lt;p&gt;&lt;strong&gt;分池之后，怎么给出一个综合的错峰结论？&lt;/strong&gt; 一个客户可能在联通很错峰，在电信却和大盘同峰。直接把各池的数字相加不合适，因为各池的成本差别很大：在贵的池子里同峰，比在便宜的池子里同峰代价高得多。所以综合指标应该&lt;strong&gt;按成本加权&lt;/strong&gt;：&lt;/p&gt;&lt;span&gt;&lt;span&gt;&lt;span&gt;k综合=∑iBi⋅ci∑iΔBi⋅ci,综合错峰成本=∑i(Bi−ΔBi)⋅cik_{\text{综合}} = \frac{\sum_i B_i \cdot c_i}{\sum_i \Delta B_i \cdot c_i}, \qquad \text{综合错峰成本} = \sum_i (B_i - \Delta B_i) \cdot c_i&lt;/span&gt;&lt;span&gt;&lt;span&gt;&lt;span&gt;&lt;/span&gt;&lt;span&gt;&lt;span&gt;k&lt;/span&gt;&lt;span&gt;&lt;span&gt;&lt;span&gt;&lt;span&gt;&lt;span&gt;&lt;span&gt;&lt;/span&gt;&lt;span&gt;&lt;span&gt;&lt;span&gt;&lt;span&gt;综合&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span&gt;​&lt;/span&gt;&lt;/span&gt;&lt;span&gt;&lt;span&gt;&lt;span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span&gt;&lt;/span&gt;&lt;span&gt;=&lt;/span&gt;&lt;span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span&gt;&lt;span&gt;&lt;/span&gt;&lt;span&gt;&lt;span&gt;&lt;/span&gt;&lt;span&gt;&lt;span&gt;&lt;span&gt;&lt;span&gt;&lt;span&gt;&lt;span&gt;&lt;/span&gt;&lt;span&gt;&lt;span&gt;&lt;span&gt;∑&lt;/span&gt;&lt;span&gt;&lt;span&gt;&lt;span&gt;&lt;span&gt;&lt;span&gt;&lt;span&gt;&lt;/span&gt;&lt;span&gt;&lt;span&gt;i&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span&gt;​&lt;/span&gt;&lt;/span&gt;&lt;span&gt;&lt;span&gt;&lt;span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span&gt;&lt;/span&gt;&lt;span&gt;Δ&lt;/span&gt;&lt;span&gt;&lt;span&gt;B&lt;/span&gt;&lt;span&gt;&lt;span&gt;&lt;span&gt;&lt;span&gt;&lt;span&gt;&lt;span&gt;&lt;/span&gt;&lt;span&gt;&lt;span&gt;i&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span&gt;​&lt;/span&gt;&lt;/span&gt;&lt;span&gt;&lt;span&gt;&lt;span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span&gt;&lt;/span&gt;&lt;span&gt;⋅&lt;/span&gt;&lt;span&gt;&lt;/span&gt;&lt;span&gt;&lt;span&gt;c&lt;/span&gt;&lt;span&gt;&lt;span&gt;&lt;span&gt;&lt;span&gt;&lt;span&gt;&lt;span&gt;&lt;/span&gt;&lt;span&gt;&lt;span&gt;i&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span&gt;​&lt;/span&gt;&lt;/span&gt;&lt;span&gt;&lt;span&gt;&lt;span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span&gt;&lt;span&gt;&lt;/span&gt;&lt;span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span&gt;&lt;span&gt;&lt;/span&gt;&lt;span&gt;&lt;span&gt;&lt;span&gt;∑&lt;/span&gt;&lt;span&gt;&lt;span&gt;&lt;span&gt;&lt;span&gt;&lt;span&gt;&lt;span&gt;&lt;/span&gt;&lt;span&gt;&lt;span&gt;i&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span&gt;​&lt;/span&gt;&lt;/span&gt;&lt;span&gt;&lt;span&gt;&lt;span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span&gt;&lt;/span&gt;&lt;span&gt;&lt;span&gt;B&lt;/span&gt;&lt;span&gt;&lt;span&gt;&lt;span&gt;&lt;span&gt;&lt;span&gt;&lt;span&gt;&lt;/span&gt;&lt;span&gt;&lt;span&gt;i&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span&gt;​&lt;/span&gt;&lt;/span&gt;&lt;span&gt;&lt;span&gt;&lt;span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span&gt;&lt;/span&gt;&lt;span&gt;⋅&lt;/span&gt;&lt;span&gt;&lt;/span&gt;&lt;span&gt;&lt;span&gt;c&lt;/span&gt;&lt;span&gt;&lt;span&gt;&lt;span&gt;&lt;span&gt;&lt;span&gt;&lt;span&gt;&lt;/span&gt;&lt;span&gt;&lt;span&gt;i&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span&gt;​&lt;/span&gt;&lt;/span&gt;&lt;span&gt;&lt;span&gt;&lt;span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span&gt;​&lt;/span&gt;&lt;/span&gt;&lt;span&gt;&lt;span&gt;&lt;span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span&gt;,&lt;/span&gt;&lt;span&gt;&lt;/span&gt;&lt;span&gt;&lt;/span&gt;&lt;span&gt;&lt;span&gt;综合错峰成本&lt;/span&gt;&lt;/span&gt;&lt;span&gt;&lt;/span&gt;&lt;span&gt;=&lt;/span&gt;&lt;span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span&gt;&lt;span&gt;&lt;/span&gt;&lt;span&gt;&lt;span&gt;&lt;span&gt;&lt;span&gt;&lt;span&gt;&lt;span&gt;&lt;/span&gt;&lt;span&gt;&lt;span&gt;i&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span&gt;&lt;span&gt;&lt;/span&gt;&lt;span&gt;&lt;span&gt;∑&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span&gt;​&lt;/span&gt;&lt;/span&gt;&lt;span&gt;&lt;span&gt;&lt;span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;&lt;span&gt;B&lt;/span&gt;&lt;span&gt;&lt;span&gt;&lt;span&gt;&lt;span&gt;&lt;span&gt;&lt;span&gt;&lt;/span&gt;&lt;span&gt;&lt;span&gt;i&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span&gt;​&lt;/span&gt;&lt;/span&gt;&lt;span&gt;&lt;span&gt;&lt;span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span&gt;&lt;/span&gt;&lt;span&gt;−&lt;/span&gt;&lt;span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span&gt;&lt;span&gt;&lt;/span&gt;&lt;span&gt;Δ&lt;/span&gt;&lt;span&gt;&lt;span&gt;B&lt;/span&gt;&lt;span&gt;&lt;span&gt;&lt;span&gt;&lt;span&gt;&lt;span&gt;&lt;span&gt;&lt;/span&gt;&lt;span&gt;&lt;span&gt;i&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span&gt;​&lt;/span&gt;&lt;/span&gt;&lt;span&gt;&lt;span&gt;&lt;span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span&gt;)&lt;/span&gt;&lt;span&gt;&lt;/span&gt;&lt;span&gt;⋅&lt;/span&gt;&lt;span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span&gt;&lt;span&gt;&lt;/span&gt;&lt;span&gt;&lt;span&gt;c&lt;/span&gt;&lt;span&gt;&lt;span&gt;&lt;span&gt;&lt;span&gt;&lt;span&gt;&lt;span&gt;&lt;/span&gt;&lt;span&gt;&lt;span&gt;i&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span&gt;​&lt;/span&gt;&lt;/span&gt;&lt;span&gt;&lt;span&gt;&lt;span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;p&gt;分子是「如果客户在每个池子都和大盘完全同峰，厂商要多付的成本」，分母是客户实际带来的增量成本，两者之比就是综合的边际超卖倍数；综合错峰成本为正，就是整体错峰。&lt;/p&gt;&lt;p&gt;这个定义代回最低价公式，结构和原来完全一样：&lt;/p&gt;&lt;span&gt;&lt;span&gt;&lt;span&gt;Pmin⁡=C加权k综合×h,C加权=∑iBiciB客户P_{\min} = \frac{C_{\text{加权}}}{k_{\text{综合}} \times h}, \qquad C_{\text{加权}} = \frac{\sum_i B_i c_i}{B_{\text{客户}}}&lt;/span&gt;&lt;span&gt;&lt;span&gt;&lt;span&gt;&lt;/span&gt;&lt;span&gt;&lt;span&gt;P&lt;/span&gt;&lt;span&gt;&lt;span&gt;&lt;span&gt;&lt;span&gt;&lt;span&gt;&lt;span&gt;&lt;/span&gt;&lt;span&gt;&lt;span&gt;&lt;span&gt;&lt;span&gt;m&lt;/span&gt;&lt;span&gt;i&lt;/span&gt;&lt;span&gt;n&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span&gt;​&lt;/span&gt;&lt;/span&gt;&lt;span&gt;&lt;span&gt;&lt;span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span&gt;&lt;/span&gt;&lt;span&gt;=&lt;/span&gt;&lt;span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span&gt;&lt;span&gt;&lt;/span&gt;&lt;span&gt;&lt;span&gt;&lt;/span&gt;&lt;span&gt;&lt;span&gt;&lt;span&gt;&lt;span&gt;&lt;span&gt;&lt;span&gt;&lt;/span&gt;&lt;span&gt;&lt;span&gt;&lt;span&gt;k&lt;/span&gt;&lt;span&gt;&lt;span&gt;&lt;span&gt;&lt;span&gt;&lt;span&gt;&lt;span&gt;&lt;/span&gt;&lt;span&gt;&lt;span&gt;&lt;span&gt;&lt;span&gt;综合&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span&gt;​&lt;/span&gt;&lt;/span&gt;&lt;span&gt;&lt;span&gt;&lt;span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span&gt;&lt;/span&gt;&lt;span&gt;×&lt;/span&gt;&lt;span&gt;&lt;/span&gt;&lt;span&gt;h&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span&gt;&lt;span&gt;&lt;/span&gt;&lt;span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span&gt;&lt;span&gt;&lt;/span&gt;&lt;span&gt;&lt;span&gt;&lt;span&gt;C&lt;/span&gt;&lt;span&gt;&lt;span&gt;&lt;span&gt;&lt;span&gt;&lt;span&gt;&lt;span&gt;&lt;/span&gt;&lt;span&gt;&lt;span&gt;&lt;span&gt;&lt;span&gt;加权&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span&gt;​&lt;/span&gt;&lt;/span&gt;&lt;span&gt;&lt;span&gt;&lt;span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span&gt;​&lt;/span&gt;&lt;/span&gt;&lt;span&gt;&lt;span&gt;&lt;span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span&gt;,&lt;/span&gt;&lt;span&gt;&lt;/span&gt;&lt;span&gt;&lt;/span&gt;&lt;span&gt;&lt;span&gt;C&lt;/span&gt;&lt;span&gt;&lt;span&gt;&lt;span&gt;&lt;span&gt;&lt;span&gt;&lt;span&gt;&lt;/span&gt;&lt;span&gt;&lt;span&gt;&lt;span&gt;&lt;span&gt;加权&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span&gt;​&lt;/span&gt;&lt;/span&gt;&lt;span&gt;&lt;span&gt;&lt;span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span&gt;&lt;/span&gt;&lt;span&gt;=&lt;/span&gt;&lt;span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span&gt;&lt;span&gt;&lt;/span&gt;&lt;span&gt;&lt;span&gt;&lt;/span&gt;&lt;span&gt;&lt;span&gt;&lt;span&gt;&lt;span&gt;&lt;span&gt;&lt;span&gt;&lt;/span&gt;&lt;span&gt;&lt;span&gt;&lt;span&gt;B&lt;/span&gt;&lt;span&gt;&lt;span&gt;&lt;span&gt;&lt;span&gt;&lt;span&gt;&lt;span&gt;&lt;/span&gt;&lt;span&gt;&lt;span&gt;&lt;span&gt;&lt;span&gt;客户&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span&gt;​&lt;/span&gt;&lt;/span&gt;&lt;span&gt;&lt;span&gt;&lt;span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span&gt;&lt;span&gt;&lt;/span&gt;&lt;span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span&gt;&lt;span&gt;&lt;/span&gt;&lt;span&gt;&lt;span&gt;&lt;span&gt;∑&lt;/span&gt;&lt;span&gt;&lt;span&gt;&lt;span&gt;&lt;span&gt;&lt;span&gt;&lt;span&gt;&lt;/span&gt;&lt;span&gt;&lt;span&gt;i&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span&gt;​&lt;/span&gt;&lt;/span&gt;&lt;span&gt;&lt;span&gt;&lt;span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span&gt;&lt;/span&gt;&lt;span&gt;&lt;span&gt;B&lt;/span&gt;&lt;span&gt;&lt;span&gt;&lt;span&gt;&lt;span&gt;&lt;span&gt;&lt;span&gt;&lt;/span&gt;&lt;span&gt;&lt;span&gt;i&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span&gt;​&lt;/span&gt;&lt;/span&gt;&lt;span&gt;&lt;span&gt;&lt;span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span&gt;&lt;span&gt;c&lt;/span&gt;&lt;span&gt;&lt;span&gt;&lt;span&gt;&lt;span&gt;&lt;span&gt;&lt;span&gt;&lt;/span&gt;&lt;span&gt;&lt;span&gt;i&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span&gt;​&lt;/span&gt;&lt;/span&gt;&lt;span&gt;&lt;span&gt;&lt;span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span&gt;​&lt;/span&gt;&lt;/span&gt;&lt;span&gt;&lt;span&gt;&lt;span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;p&gt;也就是说，原来「一个大盘」的公式，只是分池版本在只有一个池时的特例，平台的输出格式和使用者的理解方式都不用变。&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;成本 cᵢ&lt;/th&gt;&lt;th&gt;客户计费带宽 Bᵢ&lt;/th&gt;&lt;th&gt;增量 ΔBᵢ&lt;/th&gt;&lt;th&gt;分池 k&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.0&lt;/td&gt;&lt;td&gt;100&lt;/td&gt;&lt;td&gt;10&lt;/td&gt;&lt;td&gt;10&lt;/td&gt;&lt;td&gt;错峰&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;电信&lt;/td&gt;&lt;td&gt;1.2&lt;/td&gt;&lt;td&gt;60&lt;/td&gt;&lt;td&gt;60&lt;/td&gt;&lt;td&gt;1.0&lt;/td&gt;&lt;td&gt;同峰&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;非 80&lt;/td&gt;&lt;td&gt;0.6&lt;/td&gt;&lt;td&gt;40&lt;/td&gt;&lt;td&gt;5&lt;/td&gt;&lt;td&gt;8&lt;/td&gt;&lt;td&gt;错峰&lt;/td&gt;&lt;/tr&gt;&lt;/tbody&gt;&lt;/table&gt;&lt;ul&gt;
&lt;li&gt;简单相加：k = 200 ÷ 75 ≈ 2.67；&lt;/li&gt;
&lt;li&gt;按成本加权：k_综合 = (100 + 72 + 24) ÷ (10 + 72 + 3) = 196 ÷ 85 ≈ &lt;strong&gt;2.31&lt;/strong&gt;。&lt;/li&gt;
&lt;/ul&gt;&lt;p&gt;成本加权的结果更低，因为客户恰好在最贵的电信池同峰。简单相加会高估他的错峰程度，报价就可能偏低。&lt;/p&gt;&lt;p&gt;但综合指标会把信息压缩掉，而&lt;strong&gt;分池明细里往往藏着可以操作的机会&lt;/strong&gt;。上面这个客户，电信同峰、联通和非 80 错峰：&lt;/p&gt;&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;报价&lt;/strong&gt;：用综合 k 定最低价；&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;调度&lt;/strong&gt;：和客户沟通，把一部分电信流量调到联通或非 80 节点，他的综合 k 会明显上升，最低价随之下降，厂商和客户都受益；&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;资源规划&lt;/strong&gt;：如果很多新客户都在电信池同峰，说明电信大盘的高峰是瓶颈，需要优先扩容，或者优先拓展在电信池错峰的客户。&lt;/li&gt;
&lt;/ul&gt;&lt;p&gt;所以分池之后，平台的输出应该是&lt;strong&gt;一个综合结论（是否错峰、综合 k、最低价）加一张分池明细表&lt;/strong&gt;：综合指标用来定价，分池明细用来做优化。&lt;/p&gt;&lt;p&gt;&lt;strong&gt;2. 对极端的错峰结果做保护。&lt;/strong&gt; 客户甲的 k 高达 9.4，按公式算出的最低价只有资源成本的十分之一左右。当时价格只作为参考，靠「至少 7 天、稳定波形」的输入要求来保证测算可信。如果要把测算结果直接用于报价，还可以给 k 设一个上限，并加上机房、运维等固定成本的分摊，避免因为客户业务形态变化或大盘变化导致报价过低。在分资源池的版本里，给 k 设上限等价于给每个池子的增量设一个下限：ΔBᵢ 不低于客户在这个池子的计费带宽 Bᵢ 除以 k_max，也就是图中的「增量保护」。&lt;/p&gt;&lt;/section&gt;
&lt;section&gt;&lt;h2&gt;五、总结&lt;a href=&quot;#五总结&quot;&gt;&lt;span&gt;#&lt;/span&gt;&lt;/a&gt;&lt;/h2&gt;&lt;p&gt;价格测算把几件事串在了一起：&lt;/p&gt;&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;95 计费&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;/li&gt;
&lt;/ul&gt;&lt;p&gt;最终，销售拿到的不只是一个价格，而是一个判断：这个客户是错峰还是同峰，最低能报到多少，议价空间有多大。&lt;/p&gt;&lt;/section&gt;</content:encoded></item><item><title>饥荒驯牛与三元相图：把游戏设定搬进数学</title><link>https://tracerzzz.cn/posts/dst-beefalo-ternary-diagram/</link><guid isPermaLink="true">https://tracerzzz.cn/posts/dst-beefalo-ternary-diagram/</guid><description>饥荒联机版里，牛驯好后是什么形态，由战牛、行牛、肥牛三种驯势的占比决定。三个占比加起来是 100%，正好对应正三角形里任意一点到三边的距离之和恒等于高，顺着这个想法就到了三元相图，普通牛的区域是中间那个小三角。这篇记录这个发现过程，以及按游戏代码算出来的驯普通牛方法。</description><pubDate>Sun, 25 Oct 2026 13:00:00 GMT</pubDate><content:encoded>&lt;p&gt;前段时间玩《饥荒联机版》，想驯一头牛。驯牛要十几二十天，期间牛会根据你怎么对待它，慢慢偏向战牛、行牛或肥牛三种形态之一，三种都不偏就是普通牛。&lt;/p&gt;
&lt;p&gt;我先去查了 wiki 上的驯牛规则。读完之后，脑子里隐约有个三角形。这篇就是顺着这个三角形想下去的过程，后半部分是按游戏代码算出来的驯普通牛方法。&lt;/p&gt;
&lt;section&gt;&lt;h2&gt;一、wiki 上的规则&lt;a href=&quot;#一wiki-上的规则&quot;&gt;&lt;span&gt;#&lt;/span&gt;&lt;/a&gt;&lt;/h2&gt;&lt;p&gt;驯化期间，牛身上记着三个数，叫驯势点数，下文分别用三个字母表示：&lt;/p&gt;&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;O&lt;/strong&gt;：战牛（Ornery）驯势点数，骑在牛上打架时涨；&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;R&lt;/strong&gt;：行牛（Rider）驯势点数，骑着牛时涨；&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;P&lt;/strong&gt;：肥牛（Pudgy）驯势点数，牛吃得饱时涨。&lt;/li&gt;
&lt;/ul&gt;&lt;p&gt;驯化度到 100% 后再喂一次，形态就锁定了。判定只看三项的&lt;strong&gt;占比&lt;/strong&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;/p&gt;&lt;span&gt;&lt;span&gt;&lt;span&gt;o=OO+R+P,r=RO+R+P,p=PO+R+P,o+r+p=1o = \frac{O}{O+R+P},\quad r = \frac{R}{O+R+P},\quad p = \frac{P}{O+R+P},\qquad o + r + p = 1&lt;/span&gt;&lt;span&gt;&lt;span&gt;&lt;span&gt;&lt;/span&gt;&lt;span&gt;o&lt;/span&gt;&lt;span&gt;&lt;/span&gt;&lt;span&gt;=&lt;/span&gt;&lt;span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span&gt;&lt;span&gt;&lt;/span&gt;&lt;span&gt;&lt;span&gt;&lt;/span&gt;&lt;span&gt;&lt;span&gt;&lt;span&gt;&lt;span&gt;&lt;span&gt;&lt;span&gt;&lt;/span&gt;&lt;span&gt;&lt;span&gt;O&lt;/span&gt;&lt;span&gt;&lt;/span&gt;&lt;span&gt;+&lt;/span&gt;&lt;span&gt;&lt;/span&gt;&lt;span&gt;R&lt;/span&gt;&lt;span&gt;&lt;/span&gt;&lt;span&gt;+&lt;/span&gt;&lt;span&gt;&lt;/span&gt;&lt;span&gt;P&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span&gt;&lt;span&gt;&lt;/span&gt;&lt;span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span&gt;&lt;span&gt;&lt;/span&gt;&lt;span&gt;&lt;span&gt;O&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span&gt;​&lt;/span&gt;&lt;/span&gt;&lt;span&gt;&lt;span&gt;&lt;span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span&gt;,&lt;/span&gt;&lt;span&gt;&lt;/span&gt;&lt;span&gt;&lt;/span&gt;&lt;span&gt;r&lt;/span&gt;&lt;span&gt;&lt;/span&gt;&lt;span&gt;=&lt;/span&gt;&lt;span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span&gt;&lt;span&gt;&lt;/span&gt;&lt;span&gt;&lt;span&gt;&lt;/span&gt;&lt;span&gt;&lt;span&gt;&lt;span&gt;&lt;span&gt;&lt;span&gt;&lt;span&gt;&lt;/span&gt;&lt;span&gt;&lt;span&gt;O&lt;/span&gt;&lt;span&gt;&lt;/span&gt;&lt;span&gt;+&lt;/span&gt;&lt;span&gt;&lt;/span&gt;&lt;span&gt;R&lt;/span&gt;&lt;span&gt;&lt;/span&gt;&lt;span&gt;+&lt;/span&gt;&lt;span&gt;&lt;/span&gt;&lt;span&gt;P&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span&gt;&lt;span&gt;&lt;/span&gt;&lt;span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span&gt;&lt;span&gt;&lt;/span&gt;&lt;span&gt;&lt;span&gt;R&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span&gt;​&lt;/span&gt;&lt;/span&gt;&lt;span&gt;&lt;span&gt;&lt;span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span&gt;,&lt;/span&gt;&lt;span&gt;&lt;/span&gt;&lt;span&gt;&lt;/span&gt;&lt;span&gt;p&lt;/span&gt;&lt;span&gt;&lt;/span&gt;&lt;span&gt;=&lt;/span&gt;&lt;span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span&gt;&lt;span&gt;&lt;/span&gt;&lt;span&gt;&lt;span&gt;&lt;/span&gt;&lt;span&gt;&lt;span&gt;&lt;span&gt;&lt;span&gt;&lt;span&gt;&lt;span&gt;&lt;/span&gt;&lt;span&gt;&lt;span&gt;O&lt;/span&gt;&lt;span&gt;&lt;/span&gt;&lt;span&gt;+&lt;/span&gt;&lt;span&gt;&lt;/span&gt;&lt;span&gt;R&lt;/span&gt;&lt;span&gt;&lt;/span&gt;&lt;span&gt;+&lt;/span&gt;&lt;span&gt;&lt;/span&gt;&lt;span&gt;P&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span&gt;&lt;span&gt;&lt;/span&gt;&lt;span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span&gt;&lt;span&gt;&lt;/span&gt;&lt;span&gt;&lt;span&gt;P&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span&gt;​&lt;/span&gt;&lt;/span&gt;&lt;span&gt;&lt;span&gt;&lt;span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span&gt;,&lt;/span&gt;&lt;span&gt;&lt;/span&gt;&lt;span&gt;&lt;/span&gt;&lt;span&gt;o&lt;/span&gt;&lt;span&gt;&lt;/span&gt;&lt;span&gt;+&lt;/span&gt;&lt;span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span&gt;&lt;span&gt;&lt;/span&gt;&lt;span&gt;r&lt;/span&gt;&lt;span&gt;&lt;/span&gt;&lt;span&gt;+&lt;/span&gt;&lt;span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span&gt;&lt;span&gt;&lt;/span&gt;&lt;span&gt;p&lt;/span&gt;&lt;span&gt;&lt;/span&gt;&lt;span&gt;=&lt;/span&gt;&lt;span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span&gt;&lt;span&gt;&lt;/span&gt;&lt;span&gt;1&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;p&gt;比如 &lt;span&gt;&lt;span&gt;O=1, R=2, P=1O = 1,\ R = 2,\ P = 1&lt;/span&gt;&lt;span&gt;&lt;span&gt;&lt;span&gt;&lt;/span&gt;&lt;span&gt;O&lt;/span&gt;&lt;span&gt;&lt;/span&gt;&lt;span&gt;=&lt;/span&gt;&lt;span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span&gt;&lt;span&gt;&lt;/span&gt;&lt;span&gt;1&lt;/span&gt;&lt;span&gt;,&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;&lt;/span&gt;&lt;span&gt;R&lt;/span&gt;&lt;span&gt;&lt;/span&gt;&lt;span&gt;=&lt;/span&gt;&lt;span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span&gt;&lt;span&gt;&lt;/span&gt;&lt;span&gt;2&lt;/span&gt;&lt;span&gt;,&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;&lt;/span&gt;&lt;span&gt;P&lt;/span&gt;&lt;span&gt;&lt;/span&gt;&lt;span&gt;=&lt;/span&gt;&lt;span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span&gt;&lt;span&gt;&lt;/span&gt;&lt;span&gt;1&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;，占比就是 &lt;span&gt;&lt;span&gt;o=25%, r=50%, p=25%o = 25\%,\ r = 50\%,\ p = 25\%&lt;/span&gt;&lt;span&gt;&lt;span&gt;&lt;span&gt;&lt;/span&gt;&lt;span&gt;o&lt;/span&gt;&lt;span&gt;&lt;/span&gt;&lt;span&gt;=&lt;/span&gt;&lt;span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span&gt;&lt;span&gt;&lt;/span&gt;&lt;span&gt;25%&lt;/span&gt;&lt;span&gt;,&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;&lt;/span&gt;&lt;span&gt;r&lt;/span&gt;&lt;span&gt;&lt;/span&gt;&lt;span&gt;=&lt;/span&gt;&lt;span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span&gt;&lt;span&gt;&lt;/span&gt;&lt;span&gt;50%&lt;/span&gt;&lt;span&gt;,&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;&lt;/span&gt;&lt;span&gt;p&lt;/span&gt;&lt;span&gt;&lt;/span&gt;&lt;span&gt;=&lt;/span&gt;&lt;span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span&gt;&lt;span&gt;&lt;/span&gt;&lt;span&gt;25%&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;。行牛刚好占一半，这头牛驯完是行牛。&lt;/p&gt;&lt;/section&gt;
&lt;section&gt;&lt;h2&gt;二、从三角形到三元相图&lt;a href=&quot;#二从三角形到三元相图&quot;&gt;&lt;span&gt;#&lt;/span&gt;&lt;/a&gt;&lt;/h2&gt;&lt;p&gt;读完规则，我第一反应是”有点像三角形”：三个东西互相拉扯，像三角形的三个角。但具体哪里像，一开始说不上来。&lt;/p&gt;&lt;p&gt;后来反复琢磨，抓住的是 &lt;span&gt;&lt;span&gt;o+r+p=1o + r + p = 1&lt;/span&gt;&lt;span&gt;&lt;span&gt;&lt;span&gt;&lt;/span&gt;&lt;span&gt;o&lt;/span&gt;&lt;span&gt;&lt;/span&gt;&lt;span&gt;+&lt;/span&gt;&lt;span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span&gt;&lt;span&gt;&lt;/span&gt;&lt;span&gt;r&lt;/span&gt;&lt;span&gt;&lt;/span&gt;&lt;span&gt;+&lt;/span&gt;&lt;span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span&gt;&lt;span&gt;&lt;/span&gt;&lt;span&gt;p&lt;/span&gt;&lt;span&gt;&lt;/span&gt;&lt;span&gt;=&lt;/span&gt;&lt;span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span&gt;&lt;span&gt;&lt;/span&gt;&lt;span&gt;1&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt; 这一点。三个数加起来恒为 100%，这和正三角形的一个性质对得上：&lt;strong&gt;正三角形内任意一点，到三条边的距离之和恒等于三角形的高。&lt;/strong&gt;（这叫维维亚尼定理，证明很简单：把这个点和三个顶点连起来，大三角形被分成三个小三角形，三块面积加起来等于总面积，底边又都一样长，所以三个高加起来就是大三角形的高。）&lt;/p&gt;&lt;p&gt;把高当作 100%，三角形里的每个点就对应一组 &lt;span&gt;&lt;span&gt;(o,r,p)(o, r, p)&lt;/span&gt;&lt;span&gt;&lt;span&gt;&lt;span&gt;&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;o&lt;/span&gt;&lt;span&gt;,&lt;/span&gt;&lt;span&gt;&lt;/span&gt;&lt;span&gt;r&lt;/span&gt;&lt;span&gt;,&lt;/span&gt;&lt;span&gt;&lt;/span&gt;&lt;span&gt;p&lt;/span&gt;&lt;span&gt;)&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;。约定每个占比是这个点到对应顶点&lt;strong&gt;对边&lt;/strong&gt;的距离：越靠近”战牛”那个角，离它对边越远，&lt;span&gt;&lt;span&gt;oo&lt;/span&gt;&lt;span&gt;&lt;span&gt;&lt;span&gt;&lt;/span&gt;&lt;span&gt;o&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt; 越大。&lt;/p&gt;&lt;p&gt;&lt;/p&gt;&lt;figure&gt;&lt;img alt=&quot;正三角形三个顶点分别是战牛 O、行牛 R、肥牛 P，三角形内一点向三条边作垂线，长度分别是 o=20%、r=45%、p=35%，三者之和等于高，也就是 100%&quot; loading=&quot;lazy&quot; width=&quot;958&quot; height=&quot;902&quot; src=&quot;/_astro/fig0-viviani.aqtH3O5w_y84Ay.webp&quot; /&gt;&lt;figcaption&gt;正三角形三个顶点分别是战牛 O、行牛 R、肥牛 P，三角形内一点向三条边作垂线，长度分别是 o=20%、r=45%、p=35%，三者之和等于高，也就是 100%&lt;/figcaption&gt;&lt;/figure&gt;&lt;p&gt;&lt;/p&gt;&lt;p&gt;按这个思路一查，原来这东西早就有名字，叫&lt;strong&gt;三元相图&lt;/strong&gt;（ternary diagram），化学、材料、土壤分类里常用来表示三种成分的配比。&lt;/p&gt;&lt;p&gt;接下来把 wiki 里那个关键的 50% 放进图里。&lt;span&gt;&lt;span&gt;o=50%o = 50\%&lt;/span&gt;&lt;span&gt;&lt;span&gt;&lt;span&gt;&lt;/span&gt;&lt;span&gt;o&lt;/span&gt;&lt;span&gt;&lt;/span&gt;&lt;span&gt;=&lt;/span&gt;&lt;span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span&gt;&lt;span&gt;&lt;/span&gt;&lt;span&gt;50%&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt; 意味着点到对边的距离是高的一半，所有这样的点连成一条和对边平行、正好在半高处的线。这条线经过另外两条边的中点，也就是三角形的中位线。&lt;/p&gt;&lt;p&gt;三个角各有一条这样的中位线，各自切下一个角上的小三角，那里是战牛、行牛、肥牛。三条中位线围出来的，是中间一个倒着的小三角：&lt;strong&gt;普通牛区&lt;/strong&gt;。这个小三角的中心，正好是大三角形的中心，也就是 &lt;span&gt;&lt;span&gt;o=r=p=13o = r = p = \frac{1}{3}&lt;/span&gt;&lt;span&gt;&lt;span&gt;&lt;span&gt;&lt;/span&gt;&lt;span&gt;o&lt;/span&gt;&lt;span&gt;&lt;/span&gt;&lt;span&gt;=&lt;/span&gt;&lt;span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span&gt;&lt;span&gt;&lt;/span&gt;&lt;span&gt;r&lt;/span&gt;&lt;span&gt;&lt;/span&gt;&lt;span&gt;=&lt;/span&gt;&lt;span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span&gt;&lt;span&gt;&lt;/span&gt;&lt;span&gt;p&lt;/span&gt;&lt;span&gt;&lt;/span&gt;&lt;span&gt;=&lt;/span&gt;&lt;span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span&gt;&lt;span&gt;&lt;/span&gt;&lt;span&gt;&lt;span&gt;&lt;/span&gt;&lt;span&gt;&lt;span&gt;&lt;span&gt;&lt;span&gt;&lt;span&gt;&lt;span&gt;&lt;/span&gt;&lt;span&gt;&lt;span&gt;&lt;span&gt;3&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span&gt;&lt;span&gt;&lt;/span&gt;&lt;span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span&gt;&lt;span&gt;&lt;/span&gt;&lt;span&gt;&lt;span&gt;&lt;span&gt;1&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span&gt;​&lt;/span&gt;&lt;/span&gt;&lt;span&gt;&lt;span&gt;&lt;span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt; 那个点。&lt;/p&gt;&lt;p&gt;&lt;/p&gt;&lt;figure&gt;&lt;img alt=&quot;三元相图：三个角上的小三角分别是行牛、战牛、肥牛区域，中间倒三角是普通牛区，中心点是 1:1；左边中点标出肥牛点数为 0 时普通牛区缩成的那一个点&quot; loading=&quot;lazy&quot; width=&quot;1124&quot; height=&quot;902&quot; src=&quot;/_astro/fig1-ternary-regions.CKU37GPd_Z1ThsCd.webp&quot; /&gt;&lt;figcaption&gt;三元相图：三个角上的小三角分别是行牛、战牛、肥牛区域，中间倒三角是普通牛区，中心点是 1:1；左边中点标出肥牛点数为 0 时普通牛区缩成的那一个点&lt;/figcaption&gt;&lt;/figure&gt;&lt;p&gt;&lt;/p&gt;&lt;p&gt;到这里，游戏里的一条规则就完整变成了一幅几何图。我觉得这个过程挺有意思：wiki 上写的是一句话，搬到数学里，变成了正三角形、垂线和中位线。&lt;/p&gt;&lt;/section&gt;
&lt;section&gt;&lt;h2&gt;三、四块一样大，但均衡很难&lt;a href=&quot;#三四块一样大但均衡很难&quot;&gt;&lt;span&gt;#&lt;/span&gt;&lt;/a&gt;&lt;/h2&gt;&lt;p&gt;三条中位线把大三角形分成了四个全等的小三角，&lt;strong&gt;四种牛的区域面积一模一样&lt;/strong&gt;，各占四分之一。如果在三角形里随手撒一个点，落进普通牛区的机会和落进任何一个角一样大。&lt;/p&gt;&lt;p&gt;可是放到生活里，这种均衡并不多见。最像的是时间分配：一天就 24 小时，工作、家庭、自己三块加起来恒等于 100%，和 &lt;span&gt;&lt;span&gt;o+r+p=1o + r + p = 1&lt;/span&gt;&lt;span&gt;&lt;span&gt;&lt;span&gt;&lt;/span&gt;&lt;span&gt;o&lt;/span&gt;&lt;span&gt;&lt;/span&gt;&lt;span&gt;+&lt;/span&gt;&lt;span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span&gt;&lt;span&gt;&lt;/span&gt;&lt;span&gt;r&lt;/span&gt;&lt;span&gt;&lt;/span&gt;&lt;span&gt;+&lt;/span&gt;&lt;span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span&gt;&lt;span&gt;&lt;/span&gt;&lt;span&gt;p&lt;/span&gt;&lt;span&gt;&lt;/span&gt;&lt;span&gt;=&lt;/span&gt;&lt;span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span&gt;&lt;span&gt;&lt;/span&gt;&lt;span&gt;1&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&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;/p&gt;&lt;ul&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;/ul&gt;&lt;p&gt;所以四种牛里，普通牛看起来最普通，其实是最需要用心的那一种：三头都得照顾到。&lt;/p&gt;&lt;/section&gt;
&lt;section&gt;&lt;h2&gt;四、驯普通牛：按代码算&lt;a href=&quot;#四驯普通牛按代码算&quot;&gt;&lt;span&gt;#&lt;/span&gt;&lt;/a&gt;&lt;/h2&gt;&lt;p&gt;下面的数值都来自游戏脚本（&lt;code&gt;tuning.lua&lt;/code&gt;、&lt;code&gt;prefabs/beefalo.lua&lt;/code&gt;、&lt;code&gt;components/hunger.lua&lt;/code&gt;、&lt;code&gt;components/domesticatable.lua&lt;/code&gt;），换算成一天（8 分钟，480 秒）：&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;战牛 O&lt;/td&gt;&lt;td&gt;骑在牛上时，你攻击一次，或牛挨打一次&lt;/td&gt;&lt;td&gt;每次 +0.004&lt;/td&gt;&lt;td&gt;看打多少下&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;行牛 R&lt;/td&gt;&lt;td&gt;骑在牛上&lt;/td&gt;&lt;td&gt;每秒 +1/2400&lt;/td&gt;&lt;td&gt;0.2（骑满全天）&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;肥牛 P&lt;/td&gt;&lt;td&gt;饥饿值高于一半时消化食物&lt;/td&gt;&lt;td&gt;每消化 1 点饥饿 +1/2400&lt;/td&gt;&lt;td&gt;0.125（全天都高于一半）&lt;/td&gt;&lt;/tr&gt;&lt;/tbody&gt;&lt;/table&gt;&lt;p&gt;几点说明：&lt;/p&gt;&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;战牛只算骑着的时候。&lt;/strong&gt; 不骑时牛挨打，不涨战牛点数。&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;肥牛涨得比行牛慢。&lt;/strong&gt; &lt;code&gt;tuning.lua&lt;/code&gt; 里两者系数相同，都是 &lt;code&gt;1/(total_day_time*5)&lt;/code&gt;。但行牛乘的是骑行秒数；肥牛在 &lt;code&gt;beefalo.lua&lt;/code&gt; 里乘的是饥饿值的变化量：&lt;/p&gt;
&lt;div&gt;&lt;figure&gt;&lt;figcaption&gt;&lt;/figcaption&gt;&lt;pre&gt;&lt;code&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;1&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;if&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;data&lt;/span&gt;&lt;span&gt;.oldpercent &amp;gt; &lt;/span&gt;&lt;span&gt;0.5&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;then&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;2&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;    &lt;/span&gt;&lt;span&gt;inst&lt;/span&gt;&lt;span&gt;.components.&lt;/span&gt;&lt;span&gt;domesticatable&lt;/span&gt;&lt;span&gt;:&lt;/span&gt;&lt;span&gt;DeltaTendency&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;TENDENCY&lt;/span&gt;&lt;span&gt;.PUDGY, &lt;/span&gt;&lt;span&gt;TUNING&lt;/span&gt;&lt;span&gt;.BEEFALO_PUDGY_WELLFED * -&lt;/span&gt;&lt;span&gt;data&lt;/span&gt;&lt;span&gt;.delta)&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;3&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;end&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;/code&gt;&lt;/pre&gt;&lt;div&gt;&lt;div&gt;&lt;/div&gt;&lt;div&gt;&lt;/div&gt;&lt;/div&gt;&lt;/figure&gt;&lt;/div&gt;
&lt;p&gt;&lt;code&gt;hunger.lua&lt;/code&gt; 里 &lt;code&gt;delta = self.current - old&lt;/code&gt;，是饥饿值的绝对点数。牛的饥饿上限 375，每天固定掉 300，所以肥牛一天最多 &lt;span&gt;&lt;span&gt;300/2400=0.125300/2400 = 0.125&lt;/span&gt;&lt;span&gt;&lt;span&gt;&lt;span&gt;&lt;/span&gt;&lt;span&gt;300/2400&lt;/span&gt;&lt;span&gt;&lt;/span&gt;&lt;span&gt;=&lt;/span&gt;&lt;span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span&gt;&lt;span&gt;&lt;/span&gt;&lt;span&gt;0.125&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;。灰机 wiki 上写的是”每天 20%“，和代码对不上。&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;“一半”包含等于。&lt;/strong&gt; 代码里是 &lt;code&gt;maxtendencyval * 2 &amp;lt; tendencysum&lt;/code&gt; 才算普通牛，所以恰好占一半也会偏过去。另外总点数不足 0.1 时也是普通牛，不过驯满一次远超这个数。&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;驯势每天会衰减 1/45（约 0.022）。&lt;/strong&gt; 这个衰减一直在跑，不只是没人照顾的时候。&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;&lt;section&gt;&lt;h3&gt;普通牛 = 三个点数能拼成一个三角形&lt;a href=&quot;#普通牛--三个点数能拼成一个三角形&quot;&gt;&lt;span&gt;#&lt;/span&gt;&lt;/a&gt;&lt;/h3&gt;&lt;p&gt;“没有哪一项达到总数一半”，等价于”每一项都小于另外两项之和”：&lt;/p&gt;&lt;span&gt;&lt;span&gt;&lt;span&gt;O&amp;lt;R+P,R&amp;lt;O+P,P&amp;lt;O+RO &amp;lt; R + P,\qquad R &amp;lt; O + P,\qquad P &amp;lt; O + R&lt;/span&gt;&lt;span&gt;&lt;span&gt;&lt;span&gt;&lt;/span&gt;&lt;span&gt;O&lt;/span&gt;&lt;span&gt;&lt;/span&gt;&lt;span&gt;&amp;lt;&lt;/span&gt;&lt;span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span&gt;&lt;span&gt;&lt;/span&gt;&lt;span&gt;R&lt;/span&gt;&lt;span&gt;&lt;/span&gt;&lt;span&gt;+&lt;/span&gt;&lt;span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span&gt;&lt;span&gt;&lt;/span&gt;&lt;span&gt;P&lt;/span&gt;&lt;span&gt;,&lt;/span&gt;&lt;span&gt;&lt;/span&gt;&lt;span&gt;&lt;/span&gt;&lt;span&gt;R&lt;/span&gt;&lt;span&gt;&lt;/span&gt;&lt;span&gt;&amp;lt;&lt;/span&gt;&lt;span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span&gt;&lt;span&gt;&lt;/span&gt;&lt;span&gt;O&lt;/span&gt;&lt;span&gt;&lt;/span&gt;&lt;span&gt;+&lt;/span&gt;&lt;span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span&gt;&lt;span&gt;&lt;/span&gt;&lt;span&gt;P&lt;/span&gt;&lt;span&gt;,&lt;/span&gt;&lt;span&gt;&lt;/span&gt;&lt;span&gt;&lt;/span&gt;&lt;span&gt;P&lt;/span&gt;&lt;span&gt;&lt;/span&gt;&lt;span&gt;&amp;lt;&lt;/span&gt;&lt;span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span&gt;&lt;span&gt;&lt;/span&gt;&lt;span&gt;O&lt;/span&gt;&lt;span&gt;&lt;/span&gt;&lt;span&gt;+&lt;/span&gt;&lt;span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span&gt;&lt;span&gt;&lt;/span&gt;&lt;span&gt;R&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;p&gt;这正好是三角形不等式。所以普通牛还有一个很形象的判定：&lt;strong&gt;把 O、R、P 当成三根木棍的长度，能拼成一个三角形，就是普通牛。&lt;/strong&gt;&lt;/p&gt;&lt;p&gt;反过来，如果某一项为 0，比如完全不喂到半饱以上（&lt;span&gt;&lt;span&gt;P=0P = 0&lt;/span&gt;&lt;span&gt;&lt;span&gt;&lt;span&gt;&lt;/span&gt;&lt;span&gt;P&lt;/span&gt;&lt;span&gt;&lt;/span&gt;&lt;span&gt;=&lt;/span&gt;&lt;span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span&gt;&lt;span&gt;&lt;/span&gt;&lt;span&gt;0&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;），剩下两根木棍无论多长都拼不出三角形。在相图上，这对应战牛和行牛之间的那条边：普通牛区在这条边上只剩一个中点，而按代码，恰好相等也会偏过去，连这一个点都不算。所以&lt;strong&gt;三项都得有&lt;/strong&gt;，少一项就没有容错。&lt;/p&gt;&lt;/section&gt;&lt;section&gt;&lt;h3&gt;每天打多少下&lt;a href=&quot;#每天打多少下&quot;&gt;&lt;span&gt;#&lt;/span&gt;&lt;/a&gt;&lt;/h3&gt;&lt;p&gt;骑多久、喂多少一般是自己先定好的，打架次数最好调。把三角形不等式换成战斗点数的范围：&lt;/p&gt;&lt;span&gt;&lt;span&gt;&lt;span&gt;∣R−P∣&amp;lt;O&amp;lt;R+P|R - P| &amp;lt; O &amp;lt; R + P&lt;/span&gt;&lt;span&gt;&lt;span&gt;&lt;span&gt;&lt;/span&gt;&lt;span&gt;∣&lt;/span&gt;&lt;span&gt;R&lt;/span&gt;&lt;span&gt;&lt;/span&gt;&lt;span&gt;−&lt;/span&gt;&lt;span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span&gt;&lt;span&gt;&lt;/span&gt;&lt;span&gt;P&lt;/span&gt;&lt;span&gt;∣&lt;/span&gt;&lt;span&gt;&lt;/span&gt;&lt;span&gt;&amp;lt;&lt;/span&gt;&lt;span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span&gt;&lt;span&gt;&lt;/span&gt;&lt;span&gt;O&lt;/span&gt;&lt;span&gt;&lt;/span&gt;&lt;span&gt;&amp;lt;&lt;/span&gt;&lt;span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span&gt;&lt;span&gt;&lt;/span&gt;&lt;span&gt;R&lt;/span&gt;&lt;span&gt;&lt;/span&gt;&lt;span&gt;+&lt;/span&gt;&lt;span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span&gt;&lt;span&gt;&lt;/span&gt;&lt;span&gt;P&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;p&gt;这个窗口宽 &lt;span&gt;&lt;span&gt;2min⁡(R,P)2\min(R, P)&lt;/span&gt;&lt;span&gt;&lt;span&gt;&lt;span&gt;&lt;/span&gt;&lt;span&gt;2&lt;/span&gt;&lt;span&gt;&lt;/span&gt;&lt;span&gt;min&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;R&lt;/span&gt;&lt;span&gt;,&lt;/span&gt;&lt;span&gt;&lt;/span&gt;&lt;span&gt;P&lt;/span&gt;&lt;span&gt;)&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;，中点是 &lt;span&gt;&lt;span&gt;max⁡(R,P)\max(R, P)&lt;/span&gt;&lt;span&gt;&lt;span&gt;&lt;span&gt;&lt;/span&gt;&lt;span&gt;max&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;R&lt;/span&gt;&lt;span&gt;,&lt;/span&gt;&lt;span&gt;&lt;/span&gt;&lt;span&gt;P&lt;/span&gt;&lt;span&gt;)&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;。换算成每天的战斗次数（除以 0.004）：&lt;/p&gt;

&lt;table&gt;&lt;thead&gt;&lt;tr&gt;&lt;th&gt;每天怎么养&lt;/th&gt;&lt;th&gt;R/天&lt;/th&gt;&lt;th&gt;P/天&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;0.2&lt;/td&gt;&lt;td&gt;0.125&lt;/td&gt;&lt;td&gt;约 19～81&lt;/td&gt;&lt;td&gt;50&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;骑 5 分钟，饥饿值全天高于一半&lt;/td&gt;&lt;td&gt;0.125&lt;/td&gt;&lt;td&gt;0.125&lt;/td&gt;&lt;td&gt;约 0～62&lt;/td&gt;&lt;td&gt;31&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;骑满全天，饥饿值只有半天高于一半&lt;/td&gt;&lt;td&gt;0.2&lt;/td&gt;&lt;td&gt;0.0625&lt;/td&gt;&lt;td&gt;约 35～65&lt;/td&gt;&lt;td&gt;50&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;骑满全天，从不喂到一半以上&lt;/td&gt;&lt;td&gt;0.2&lt;/td&gt;&lt;td&gt;0&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;/p&gt;&lt;figure&gt;&lt;img alt=&quot;骑满全天且饥饿值全天高于一半时，随着每天战斗次数增加，点从行牛和肥牛之间的边出发，穿过普通牛区进入战牛区；进出普通牛区的位置大约是每天 19 次和 81 次，中点是每天 50 次&quot; loading=&quot;lazy&quot; width=&quot;974&quot; height=&quot;902&quot; src=&quot;/_astro/fig2-full-ride-line.D_zjYcRR_ZBlxOt.webp&quot; /&gt;&lt;figcaption&gt;骑满全天且饥饿值全天高于一半时，随着每天战斗次数增加，点从行牛和肥牛之间的边出发，穿过普通牛区进入战牛区；进出普通牛区的位置大约是每天 19 次和 81 次，中点是每天 50 次&lt;/figcaption&gt;&lt;/figure&gt;&lt;p&gt;&lt;/p&gt;&lt;p&gt;第二种是我最推荐的：&lt;strong&gt;每天骑 5 分钟&lt;/strong&gt;（300 秒 × 1/2400 = 0.125，正好等于全天饱食的肥牛点数），让 &lt;span&gt;&lt;span&gt;R=PR = P&lt;/span&gt;&lt;span&gt;&lt;span&gt;&lt;span&gt;&lt;/span&gt;&lt;span&gt;R&lt;/span&gt;&lt;span&gt;&lt;/span&gt;&lt;span&gt;=&lt;/span&gt;&lt;span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span&gt;&lt;span&gt;&lt;/span&gt;&lt;span&gt;P&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;，再在骑行时打 31 下左右，三项就落在三角形正中心。这时窗口是 0～62 次，下限直接降到 0。在 R 和 P 总量差不多的方案里，这是最宽的。哪天多打少打十几下，都不会出事。&lt;/p&gt;&lt;p&gt;算上每天约 0.022 的衰减，下限会抬高、上限会压低，各收窄约 6 下：第一种大约 24～76 次，第二种大约 6～57 次。&lt;/p&gt;&lt;p&gt;窗口宽度只取决于 R 和 P 中较小的那个。想要更大的容错，就补短板：肥牛少了就多喂，行牛少了就多骑。每多让 100 点饥饿在半线以上被消化（约一份蒸树枝），P 多 1/24，战斗次数的窗口两边各宽约 10 下。&lt;/p&gt;&lt;/section&gt;&lt;section&gt;&lt;h3&gt;怎么让饥饿值全天高于一半&lt;a href=&quot;#怎么让饥饿值全天高于一半&quot;&gt;&lt;span&gt;#&lt;/span&gt;&lt;/a&gt;&lt;/h3&gt;&lt;p&gt;饥饿上限 375，一半是 187.5，每天掉 300，也就是每三分之一天（160 秒）掉 100。&lt;/p&gt;&lt;p&gt;我的做法是：&lt;strong&gt;早上把牛喂到 300，之后每三分之一天补 100。&lt;/strong&gt; 饥饿值会在 200 到 300 之间来回，全天都高于 187.5，一天正好喂 300，大约三份蒸树枝。&lt;/p&gt;&lt;p&gt;喂食时有两个边界值得注意：&lt;/p&gt;&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;喂完超过 300（80%）会放屁。&lt;/strong&gt; 这只是提醒快饱了，没有惩罚。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;饥饿值满 375 时再喂会吐。&lt;/strong&gt; 代价是驯化度 −1%、顺从度 −30%，同时肥牛点数 +0.02。想要普通牛，要避开这一下。&lt;/li&gt;
&lt;/ul&gt;&lt;p&gt;每次喂食还能 +10% 顺从度，顺带解决骑行时被甩下来的问题。&lt;/p&gt;&lt;/section&gt;&lt;section&gt;&lt;h3&gt;其他&lt;a href=&quot;#其他&quot;&gt;&lt;span&gt;#&lt;/span&gt;&lt;/a&gt;&lt;/h3&gt;&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;驯化度&lt;/strong&gt;：牛被骑着、或饥饿值大于 0 时，每 10 秒 +1/960，20 天驯满；每天刷一次毛再 +1/60，能缩到 15 天。上面的每日窗口和总天数无关，15 天、20 天都一样用。&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;/ul&gt;&lt;blockquote&gt;&lt;p&gt;关于这篇文章：游戏数值来自 GitHub 上的游戏脚本镜像 &lt;a href=&quot;https://github.com/CN-DST-DEVELOPER/scripts&quot; target=&quot;_blank&quot;&gt;CN-DST-DEVELOPER/scripts&lt;/a&gt;，规则描述参考了&lt;a href=&quot;https://dontstarve.huijiwiki.com/wiki/%E7%9A%AE%E5%BC%97%E5%A8%84%E7%89%9B&quot; target=&quot;_blank&quot;&gt;灰机 wiki 的皮弗娄牛页面&lt;/a&gt;和 &lt;a href=&quot;https://dontstarve.wiki.gg/wiki/Beefalo/DST&quot; target=&quot;_blank&quot;&gt;wiki.gg 的 Beefalo/DST 页面&lt;/a&gt;，查阅于 2026 年 10 月。游戏更新后数值可能会变。&lt;/p&gt;&lt;/blockquote&gt;&lt;/section&gt;&lt;/section&gt;</content:encoded></item><item><title>CDN 的错峰与超卖：为什么厂商能把同一份带宽卖好几次</title><link>https://tracerzzz.cn/posts/cdn-off-peak-and-overselling/</link><guid isPermaLink="true">https://tracerzzz.cn/posts/cdn-off-peak-and-overselling/</guid><description>在 95 计费下，一个新客户接入后，大盘的 95 可能几乎不涨，这就是错峰客户。它让 CDN 厂商可以把同一份带宽卖给多个客户，也就是超卖。这篇用模拟数据讲清楚错峰和超卖，以及衡量它们的两个指标：单个客户的边际超卖倍数，和整个平台的超售比。</description><pubDate>Sun, 25 Oct 2026 12:00:00 GMT</pubDate><content:encoded>&lt;p&gt;在&lt;a href=&quot;/posts/cdn-billing-methods/&quot;&gt;《CDN 是怎么收费的》&lt;/a&gt;里讲过，CDN 厂商向上游运营商付费、向大客户收费，绝大多数都按 95 峰值结算。这背后藏着一个很有意思的现象：&lt;strong&gt;厂商向所有客户收的带宽加起来，往往比它向上游付费的带宽多。&lt;/strong&gt; 同一份带宽，被卖了不止一次。&lt;/p&gt;
&lt;p&gt;这篇讲清楚这是怎么回事，以及在经营分析里怎么衡量它。&lt;/p&gt;
&lt;blockquote&gt;&lt;p&gt;文中的数据都是模拟的。&lt;/p&gt;&lt;/blockquote&gt;
&lt;section&gt;&lt;h2&gt;一、大盘与 95&lt;a href=&quot;#一大盘与-95&quot;&gt;&lt;span&gt;#&lt;/span&gt;&lt;/a&gt;&lt;/h2&gt;&lt;p&gt;厂商的所有客户共用同一批节点和出口带宽。把所有客户的带宽在每个 5 分钟点上加起来，就是&lt;strong&gt;大盘&lt;/strong&gt;的带宽曲线。厂商向上游付费，看的是大盘的 95。&lt;/p&gt;&lt;p&gt;模拟的大盘一天有两个高峰：中午一个小高峰，晚上 21 点左右一个大高峰。一个月下来，大盘的 95 约为 &lt;strong&gt;679 Mbps&lt;/strong&gt;。&lt;/p&gt;&lt;/section&gt;
&lt;section&gt;&lt;h2&gt;二、错峰：接入了新客户，成本却几乎没涨&lt;a href=&quot;#二错峰接入了新客户成本却几乎没涨&quot;&gt;&lt;span&gt;#&lt;/span&gt;&lt;/a&gt;&lt;/h2&gt;&lt;p&gt;现在有两个规模差不多的新客户要接入，他们自己的 95 都在 &lt;strong&gt;208 Mbps&lt;/strong&gt; 左右，区别只在高峰时间：&lt;/p&gt;&lt;ul&gt;
&lt;li&gt;客户甲：业务高峰在&lt;strong&gt;上午 10 点&lt;/strong&gt;；&lt;/li&gt;
&lt;li&gt;客户乙：业务高峰在&lt;strong&gt;晚上 21 点&lt;/strong&gt;，和大盘一样。&lt;/li&gt;
&lt;/ul&gt;&lt;p&gt;分别把他们的曲线叠加到大盘上，重新算 95：&lt;/p&gt;

&lt;table&gt;&lt;thead&gt;&lt;tr&gt;&lt;th&gt;&lt;/th&gt;&lt;th&gt;客户自己的 95&lt;/th&gt;&lt;th&gt;接入后大盘 95&lt;/th&gt;&lt;th&gt;大盘 95 增量&lt;/th&gt;&lt;/tr&gt;&lt;/thead&gt;&lt;tbody&gt;&lt;tr&gt;&lt;td&gt;客户甲（上午高峰）&lt;/td&gt;&lt;td&gt;208&lt;/td&gt;&lt;td&gt;702&lt;/td&gt;&lt;td&gt;&lt;strong&gt;22&lt;/strong&gt;&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;客户乙（晚上高峰）&lt;/td&gt;&lt;td&gt;207&lt;/td&gt;&lt;td&gt;888&lt;/td&gt;&lt;td&gt;&lt;strong&gt;208&lt;/strong&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;客户甲的高峰正好落在大盘的低谷里，叠加之后，大盘的最高那些点几乎没变，95 只涨了 22；&lt;/li&gt;
&lt;li&gt;客户乙的高峰和大盘的高峰叠在一起，大盘 95 实打实地涨了 208，和他自己的 95 一样多。&lt;/li&gt;
&lt;/ul&gt;&lt;p&gt;客户甲就是&lt;strong&gt;错峰客户&lt;/strong&gt;。对厂商来说，接入他几乎不增加上游成本。&lt;/p&gt;&lt;p&gt;判断一个客户是否错峰，最直接的办法是看他的计费带宽和他带来的增量之差：&lt;/p&gt;&lt;span&gt;&lt;span&gt;&lt;span&gt;错峰带宽=B客户−ΔB\text{错峰带宽} = B_{\text{客户}} - \Delta B&lt;/span&gt;&lt;span&gt;&lt;span&gt;&lt;span&gt;&lt;/span&gt;&lt;span&gt;&lt;span&gt;错峰带宽&lt;/span&gt;&lt;/span&gt;&lt;span&gt;&lt;/span&gt;&lt;span&gt;=&lt;/span&gt;&lt;span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span&gt;&lt;span&gt;&lt;/span&gt;&lt;span&gt;&lt;span&gt;B&lt;/span&gt;&lt;span&gt;&lt;span&gt;&lt;span&gt;&lt;span&gt;&lt;span&gt;&lt;span&gt;&lt;/span&gt;&lt;span&gt;&lt;span&gt;&lt;span&gt;&lt;span&gt;客户&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span&gt;​&lt;/span&gt;&lt;/span&gt;&lt;span&gt;&lt;span&gt;&lt;span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span&gt;&lt;/span&gt;&lt;span&gt;−&lt;/span&gt;&lt;span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span&gt;&lt;span&gt;&lt;/span&gt;&lt;span&gt;Δ&lt;/span&gt;&lt;span&gt;B&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;ul&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;/ul&gt;&lt;p&gt;客户甲的错峰带宽是 208 − 22 = 186，明显为正；客户乙是 207 − 208 ≈ −1，没有错峰。&lt;/p&gt;&lt;/section&gt;
&lt;section&gt;&lt;h2&gt;三、超卖：同一份带宽卖了好几次&lt;a href=&quot;#三超卖同一份带宽卖了好几次&quot;&gt;&lt;span&gt;#&lt;/span&gt;&lt;/a&gt;&lt;/h2&gt;&lt;p&gt;厂商向客户收费，按的是&lt;strong&gt;客户自己的 95&lt;/strong&gt;；向上游付费，按的是&lt;strong&gt;大盘的 95&lt;/strong&gt;。&lt;/p&gt;&lt;p&gt;接入客户甲之后：&lt;/p&gt;&lt;ul&gt;
&lt;li&gt;厂商向客户甲收 &lt;strong&gt;208&lt;/strong&gt; 的带宽费；&lt;/li&gt;
&lt;li&gt;厂商向上游多付的只有 &lt;strong&gt;22&lt;/strong&gt;。&lt;/li&gt;
&lt;/ul&gt;&lt;p&gt;多出来的部分，就是超卖。本质上，客户甲用的是其他客户在上午闲置下来的带宽，厂商把同一份带宽在不同的时段卖给了不同的人。&lt;/p&gt;&lt;p&gt;这不是什么不正当的事，而是共享基础设施的天然收益，和航空公司卖机票、电信运营商卖宽带是一个道理。&lt;strong&gt;一家 CDN 厂商的利润，很大程度上就来自客户结构的互补&lt;/strong&gt;：白天的办公、电商，晚上的视频、游戏，海外和国内的时差，这些都在帮厂商把同一份带宽复用起来。&lt;/p&gt;&lt;/section&gt;
&lt;section&gt;&lt;h2&gt;四、两个衡量指标&lt;a href=&quot;#四两个衡量指标&quot;&gt;&lt;span&gt;#&lt;/span&gt;&lt;/a&gt;&lt;/h2&gt;&lt;section&gt;&lt;h3&gt;1. 边际超卖倍数：这个客户有多错峰&lt;a href=&quot;#1-边际超卖倍数这个客户有多错峰&quot;&gt;&lt;span&gt;#&lt;/span&gt;&lt;/a&gt;&lt;/h3&gt;&lt;p&gt;衡量单个客户，用他的计费带宽和他带来的增量之比：&lt;/p&gt;&lt;span&gt;&lt;span&gt;&lt;span&gt;k=B客户ΔBk = \frac{B_{\text{客户}}}{\Delta B}&lt;/span&gt;&lt;span&gt;&lt;span&gt;&lt;span&gt;&lt;/span&gt;&lt;span&gt;k&lt;/span&gt;&lt;span&gt;&lt;/span&gt;&lt;span&gt;=&lt;/span&gt;&lt;span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span&gt;&lt;span&gt;&lt;/span&gt;&lt;span&gt;&lt;span&gt;&lt;/span&gt;&lt;span&gt;&lt;span&gt;&lt;span&gt;&lt;span&gt;&lt;span&gt;&lt;span&gt;&lt;/span&gt;&lt;span&gt;&lt;span&gt;Δ&lt;/span&gt;&lt;span&gt;B&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span&gt;&lt;span&gt;&lt;/span&gt;&lt;span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span&gt;&lt;span&gt;&lt;/span&gt;&lt;span&gt;&lt;span&gt;&lt;span&gt;B&lt;/span&gt;&lt;span&gt;&lt;span&gt;&lt;span&gt;&lt;span&gt;&lt;span&gt;&lt;span&gt;&lt;/span&gt;&lt;span&gt;&lt;span&gt;&lt;span&gt;&lt;span&gt;客户&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span&gt;​&lt;/span&gt;&lt;/span&gt;&lt;span&gt;&lt;span&gt;&lt;span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span&gt;​&lt;/span&gt;&lt;/span&gt;&lt;span&gt;&lt;span&gt;&lt;span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;ul&gt;
&lt;li&gt;客户甲：208 ÷ 22 ≈ &lt;strong&gt;9.4&lt;/strong&gt;，每多付 1 份上游带宽，能收 9 份多的钱；&lt;/li&gt;
&lt;li&gt;客户乙：207 ÷ 208 ≈ &lt;strong&gt;1.0&lt;/strong&gt;，收多少就要多付多少，没有超卖空间。&lt;/li&gt;
&lt;/ul&gt;&lt;p&gt;k &amp;gt; 1 和错峰带宽为正是等价的，k 越大，越错峰。&lt;/p&gt;&lt;p&gt;这里有一个容易混淆的地方：「接入后大盘 95 涨了百分之几」也是一个有用的数，可以用来评估接入的影响，但它&lt;strong&gt;不能衡量超卖&lt;/strong&gt;。因为它没考虑客户自己有多大：一个很大的客户让大盘涨了 10%，看起来影响不小，但如果他自己的 95 是这个增量的三倍，他其实非常错峰。所以衡量超卖，要用客户的计费带宽去比增量，而不是用增量去比原来的大盘。&lt;/p&gt;&lt;/section&gt;&lt;section&gt;&lt;h3&gt;2. 超售比：整个平台卖出了多少倍&lt;a href=&quot;#2-超售比整个平台卖出了多少倍&quot;&gt;&lt;span&gt;#&lt;/span&gt;&lt;/a&gt;&lt;/h3&gt;&lt;p&gt;衡量整个平台，用所有客户的计费带宽之和，比上大盘的计费带宽：&lt;/p&gt;&lt;span&gt;&lt;span&gt;&lt;span&gt;超售比=∑各客户的计费带宽大盘计费带宽\text{超售比} = \frac{\sum \text{各客户的计费带宽}}{\text{大盘计费带宽}}&lt;/span&gt;&lt;span&gt;&lt;span&gt;&lt;span&gt;&lt;/span&gt;&lt;span&gt;&lt;span&gt;超售比&lt;/span&gt;&lt;/span&gt;&lt;span&gt;&lt;/span&gt;&lt;span&gt;=&lt;/span&gt;&lt;span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span&gt;&lt;span&gt;&lt;/span&gt;&lt;span&gt;&lt;span&gt;&lt;/span&gt;&lt;span&gt;&lt;span&gt;&lt;span&gt;&lt;span&gt;&lt;span&gt;&lt;span&gt;&lt;/span&gt;&lt;span&gt;&lt;span&gt;&lt;span&gt;大盘计费带宽&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span&gt;&lt;span&gt;&lt;/span&gt;&lt;span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span&gt;&lt;span&gt;&lt;/span&gt;&lt;span&gt;&lt;span&gt;∑&lt;/span&gt;&lt;span&gt;&lt;/span&gt;&lt;span&gt;&lt;span&gt;各客户的计费带宽&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span&gt;​&lt;/span&gt;&lt;/span&gt;&lt;span&gt;&lt;span&gt;&lt;span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;p&gt;以接入客户甲为例，假设原来的大盘里各客户的计费带宽之和正好等于 679（实际上通常已经大于它），接入后超售比约为 (679 + 208) ÷ 702 ≈ &lt;strong&gt;1.26&lt;/strong&gt;，即向客户收的带宽比向上游付的多 26%。&lt;/p&gt;&lt;p&gt;超售比是经营分析里的核心指标之一：它越高，说明带宽复用得越好，毛利空间越大。我在百度做 CDN 成本优化时，超售比就是日常跟踪的指标之一。&lt;/p&gt;&lt;/section&gt;&lt;/section&gt;
&lt;section&gt;&lt;h2&gt;五、错峰客户给成本分摊带来的难题&lt;a href=&quot;#五错峰客户给成本分摊带来的难题&quot;&gt;&lt;span&gt;#&lt;/span&gt;&lt;/a&gt;&lt;/h2&gt;&lt;p&gt;错峰带来了利润，也给&lt;strong&gt;成本分摊&lt;/strong&gt;出了难题。&lt;/p&gt;&lt;p&gt;如果按传统的单点法分摊成本，也就是只看大盘 95 时刻那一个点，每个客户在那一刻用了多少带宽、就承担多少比例的成本，那么错峰客户在 95 时刻往往几乎没有用量，就会&lt;strong&gt;几乎不承担任何成本&lt;/strong&gt;。在经营分析里，他会显得毛利极高，而同峰客户则承担了几乎全部成本。&lt;/p&gt;&lt;p&gt;但错峰客户并不是完全不占资源：在月内其他的高峰时段，他们多少也贡献了一些用量。单点法把这部分完全忽略了，而且结果还取决于恰好选中了哪个点。&lt;/p&gt;&lt;p&gt;这正是我设计「95 区间」分摊算法要解决的问题之一：在 95 值附近取一段区间，按各客户在整段区间里的用量来分摊，错峰客户也能分到和他实际占用相符的成本。详见&lt;a href=&quot;/posts/cdn-bandwidth-cost-allocation-patent/&quot;&gt;《我的第一项专利：用「95 区间」把 CDN 带宽成本分得更准》&lt;/a&gt;。&lt;/p&gt;&lt;/section&gt;
&lt;section&gt;&lt;h2&gt;六、在业务上怎么用&lt;a href=&quot;#六在业务上怎么用&quot;&gt;&lt;span&gt;#&lt;/span&gt;&lt;/a&gt;&lt;/h2&gt;&lt;p&gt;理解了错峰和超卖，很多经营决策就有了依据：&lt;/p&gt;&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;报价&lt;/strong&gt;：错峰客户给厂商带来的增量成本低，可以给更有竞争力的价格；同峰客户则不能低于成本线。下一篇&lt;a href=&quot;/posts/cdn-new-customer-pricing/&quot;&gt;《CDN 新客户接入前的价格测算》&lt;/a&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;/ul&gt;&lt;/section&gt;</content:encoded></item><item><title>CDN 是怎么收费的：从流量、95 峰值到日峰月均，厂商和客户两边看计费</title><link>https://tracerzzz.cn/posts/cdn-billing-methods/</link><guid isPermaLink="true">https://tracerzzz.cn/posts/cdn-billing-methods/</guid><description>我在百度云 CDN 做过客户账单，在字节做过火山 CDN 的计费推量，也做过融合 CDN 向多家厂商采购的对账和成本分摊，厂商和采购两边都经历过。这篇整理 CDN 常见的计费方式：流量、带宽峰值、95 峰值、日峰月均、月第四峰值、夜间减半、闲忙时、请求数，以及计费区域、包头系数这些容易忽略的细节，并用一个月的模拟数据对比不同方式算出来差多少。</description><pubDate>Sun, 25 Oct 2026 11:00:00 GMT</pubDate><content:encoded>&lt;p&gt;在 CDN 行业做了几年，计费几乎贯穿了我所有的工作：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;在&lt;strong&gt;百度云 CDN&lt;/strong&gt;，给头部客户出定制化账单，实现过 95 峰值、日峰月均、流量、夜间带宽减半、闲忙时等多种计费方式；&lt;/li&gt;
&lt;li&gt;在&lt;strong&gt;字节火山 CDN&lt;/strong&gt;，做过把客户用量推给计费中心的计量系统；&lt;/li&gt;
&lt;li&gt;在&lt;strong&gt;字节融合 CDN&lt;/strong&gt;，站在采购方一侧，和多家 CDN 厂商对账，再把采购成本分摊给内部业务。&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;同一套计费规则，厂商关心的是「怎么收」，客户关心的是「怎么付得最少」。这篇把常见的计费方式整理一遍，并用一个月的模拟数据算一算，不同方式到底差多少。&lt;/p&gt;
&lt;blockquote&gt;&lt;p&gt;说明：各家厂商的具体规则（采样粒度、时段划分、取整方式等）以合同和官网为准，本文讲的是通用的思路。文中的数据都是模拟的。&lt;/p&gt;&lt;/blockquote&gt;
&lt;section&gt;&lt;h2&gt;一、两大类：按流量，还是按带宽&lt;a href=&quot;#一两大类按流量还是按带宽&quot;&gt;&lt;span&gt;#&lt;/span&gt;&lt;/a&gt;&lt;/h2&gt;&lt;p&gt;CDN 的计费方式，归根结底分两类：&lt;/p&gt;&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;按流量&lt;/strong&gt;：这个月一共传了多少数据（GB、TB），用多少付多少；&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;按带宽&lt;/strong&gt;：这个月「最高」用到了多大的带宽（Mbps、Gbps），按这个高度付费。至于「最高」怎么定义，就衍生出了下面的各种方式。&lt;/li&gt;
&lt;/ul&gt;&lt;p&gt;为什么会有按带宽计费？因为 CDN 厂商自己的成本，很大一部分就是按带宽算的：机房、运营商出口都要按峰值准备容量，峰值再短，资源也得提前备好。按带宽向客户收费，是把这个成本结构传导下去。&lt;/p&gt;&lt;/section&gt;
&lt;section&gt;&lt;h2&gt;二、计费的基础：5 分钟一个点&lt;a href=&quot;#二计费的基础5-分钟一个点&quot;&gt;&lt;span&gt;#&lt;/span&gt;&lt;/a&gt;&lt;/h2&gt;&lt;p&gt;除了纯流量计费，几乎所有带宽类的计费都基于同一份数据：&lt;strong&gt;每 5 分钟一个带宽点&lt;/strong&gt;。&lt;/p&gt;&lt;p&gt;做法是统计这 5 分钟内的总流量，除以 300 秒，得到这 5 分钟的平均带宽：&lt;/p&gt;&lt;span&gt;&lt;span&gt;&lt;span&gt;带宽5min⁡=5 分钟内的流量（bit）300 s\text{带宽}_{5\min} = \frac{\text{5 分钟内的流量（bit）}}{300\ \text{s}}&lt;/span&gt;&lt;span&gt;&lt;span&gt;&lt;span&gt;&lt;/span&gt;&lt;span&gt;&lt;span&gt;&lt;span&gt;带宽&lt;/span&gt;&lt;/span&gt;&lt;span&gt;&lt;span&gt;&lt;span&gt;&lt;span&gt;&lt;span&gt;&lt;span&gt;&lt;/span&gt;&lt;span&gt;&lt;span&gt;&lt;span&gt;5&lt;/span&gt;&lt;span&gt;&lt;/span&gt;&lt;span&gt;&lt;span&gt;m&lt;/span&gt;&lt;span&gt;i&lt;/span&gt;&lt;span&gt;n&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span&gt;​&lt;/span&gt;&lt;/span&gt;&lt;span&gt;&lt;span&gt;&lt;span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span&gt;&lt;/span&gt;&lt;span&gt;=&lt;/span&gt;&lt;span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span&gt;&lt;span&gt;&lt;/span&gt;&lt;span&gt;&lt;span&gt;&lt;/span&gt;&lt;span&gt;&lt;span&gt;&lt;span&gt;&lt;span&gt;&lt;span&gt;&lt;span&gt;&lt;/span&gt;&lt;span&gt;&lt;span&gt;300&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;&lt;span&gt;s&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span&gt;&lt;span&gt;&lt;/span&gt;&lt;span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span&gt;&lt;span&gt;&lt;/span&gt;&lt;span&gt;&lt;span&gt;&lt;span&gt;5 &lt;/span&gt;&lt;span&gt;分钟内的流量（&lt;/span&gt;&lt;span&gt;bit&lt;/span&gt;&lt;span&gt;）&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span&gt;​&lt;/span&gt;&lt;/span&gt;&lt;span&gt;&lt;span&gt;&lt;span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;p&gt;一天 288 个点，30 天的月份 8640 个点，所有带宽类计费都是在这 8640 个点上做文章。我在火山 CDN 做计费推量时，带宽计费的客户就是每 5 分钟推一个点给计费中心。&lt;/p&gt;&lt;p&gt;为了让后面的对比有个参照，我模拟了一个客户一个月的带宽：白天有个小高峰，晚上 21 点左右是大高峰，每天的波动在 ±5% 以内；另外第 11 天晚上有一场直播活动，带宽在一个小时里冲到了平时的两倍多。&lt;/p&gt;&lt;/section&gt;
&lt;section&gt;&lt;h2&gt;三、按流量计费&lt;a href=&quot;#三按流量计费&quot;&gt;&lt;span&gt;#&lt;/span&gt;&lt;/a&gt;&lt;/h2&gt;&lt;p&gt;最直观的方式：把一个月所有的流量加起来，乘以单价。单价通常是&lt;strong&gt;阶梯价&lt;/strong&gt;，用量越大越便宜。&lt;/p&gt;&lt;p&gt;模拟客户一个月的平均带宽约 472 Mbps，对应的流量约 &lt;strong&gt;153 TB&lt;/strong&gt;。&lt;/p&gt;&lt;p&gt;带宽和流量之间有一个很好记的换算：&lt;strong&gt;1 Mbps 的带宽跑满一个 30 天的月，大约是 324 GB 流量&lt;/strong&gt;。&lt;/p&gt;&lt;span&gt;&lt;span&gt;&lt;span&gt;1 Mbps×30 天=106×2,592,0008 Byte≈324 GB1\ \text{Mbps} \times 30\ \text{天} = \frac{10^6 \times 2{,}592{,}000}{8}\ \text{Byte} \approx 324\ \text{GB}&lt;/span&gt;&lt;span&gt;&lt;span&gt;&lt;span&gt;&lt;/span&gt;&lt;span&gt;1&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;&lt;span&gt;Mbps&lt;/span&gt;&lt;/span&gt;&lt;span&gt;&lt;/span&gt;&lt;span&gt;×&lt;/span&gt;&lt;span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span&gt;&lt;span&gt;&lt;/span&gt;&lt;span&gt;30&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;&lt;span&gt;天&lt;/span&gt;&lt;/span&gt;&lt;span&gt;&lt;/span&gt;&lt;span&gt;=&lt;/span&gt;&lt;span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span&gt;&lt;span&gt;&lt;/span&gt;&lt;span&gt;&lt;span&gt;&lt;/span&gt;&lt;span&gt;&lt;span&gt;&lt;span&gt;&lt;span&gt;&lt;span&gt;&lt;span&gt;&lt;/span&gt;&lt;span&gt;&lt;span&gt;8&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span&gt;&lt;span&gt;&lt;/span&gt;&lt;span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span&gt;&lt;span&gt;&lt;/span&gt;&lt;span&gt;&lt;span&gt;1&lt;/span&gt;&lt;span&gt;&lt;span&gt;0&lt;/span&gt;&lt;span&gt;&lt;span&gt;&lt;span&gt;&lt;span&gt;&lt;span&gt;&lt;span&gt;&lt;/span&gt;&lt;span&gt;&lt;span&gt;6&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span&gt;&lt;/span&gt;&lt;span&gt;×&lt;/span&gt;&lt;span&gt;&lt;/span&gt;&lt;span&gt;2&lt;/span&gt;&lt;span&gt;&lt;span&gt;,&lt;/span&gt;&lt;/span&gt;&lt;span&gt;592&lt;/span&gt;&lt;span&gt;&lt;span&gt;,&lt;/span&gt;&lt;/span&gt;&lt;span&gt;000&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span&gt;​&lt;/span&gt;&lt;/span&gt;&lt;span&gt;&lt;span&gt;&lt;span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;&lt;span&gt;Byte&lt;/span&gt;&lt;/span&gt;&lt;span&gt;&lt;/span&gt;&lt;span&gt;≈&lt;/span&gt;&lt;span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span&gt;&lt;span&gt;&lt;/span&gt;&lt;span&gt;324&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;&lt;span&gt;GB&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;p&gt;这个数后面算「该选哪种计费方式」时会用到。&lt;/p&gt;&lt;/section&gt;
&lt;section&gt;&lt;h2&gt;四、按带宽计费的几种方式&lt;a href=&quot;#四按带宽计费的几种方式&quot;&gt;&lt;span&gt;#&lt;/span&gt;&lt;/a&gt;&lt;/h2&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;计费带宽（Mbps）&lt;/th&gt;&lt;/tr&gt;&lt;/thead&gt;&lt;tbody&gt;&lt;tr&gt;&lt;td&gt;月峰值&lt;/td&gt;&lt;td&gt;8640 个点里的最大值&lt;/td&gt;&lt;td&gt;1589&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;日峰值月平均&lt;/td&gt;&lt;td&gt;每天取最大值，30 个值求平均&lt;/td&gt;&lt;td&gt;756&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;月第四峰值&lt;/td&gt;&lt;td&gt;每天取最大值，30 个值里排第四大的&lt;/td&gt;&lt;td&gt;734&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;95 峰值&lt;/td&gt;&lt;td&gt;8640 个点去掉最高的 5%，剩下的最大值&lt;/td&gt;&lt;td&gt;678&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;（参考）月平均带宽&lt;/td&gt;&lt;td&gt;8640 个点的平均值&lt;/td&gt;&lt;td&gt;472&lt;/td&gt;&lt;/tr&gt;&lt;/tbody&gt;&lt;/table&gt;&lt;p&gt;同一个客户、同一个月，不同算法的计费带宽能差一倍多。下面逐个说。&lt;/p&gt;&lt;section&gt;&lt;h3&gt;1. 月峰值：最简单，也最「贵」&lt;a href=&quot;#1-月峰值最简单也最贵&quot;&gt;&lt;span&gt;#&lt;/span&gt;&lt;/a&gt;&lt;/h3&gt;&lt;p&gt;直接取整个月的最大值。问题很明显：第 11 天那一小时的直播，把计费带宽从六七百拉到了将近 1600，客户要为一个小时的尖峰付一整个月的钱。所以纯月峰值计费很少见。&lt;/p&gt;&lt;/section&gt;&lt;section&gt;&lt;h3&gt;2. 95 峰值：行业最主流&lt;a href=&quot;#2-95-峰值行业最主流&quot;&gt;&lt;span&gt;#&lt;/span&gt;&lt;/a&gt;&lt;/h3&gt;&lt;p&gt;把 8640 个点从大到小排序，去掉最高的 5%（432 个点），第 433 个点就是 95 峰值。&lt;/p&gt;&lt;span&gt;&lt;span&gt;&lt;span&gt;95 峰值=降序排列后的第 ⌊8640×5%⌋+1=433 个点\text{95 峰值} = \text{降序排列后的第}\ \lfloor 8640 \times 5\% \rfloor + 1 = 433\ \text{个点}&lt;/span&gt;&lt;span&gt;&lt;span&gt;&lt;span&gt;&lt;/span&gt;&lt;span&gt;&lt;span&gt;95 &lt;/span&gt;&lt;span&gt;峰值&lt;/span&gt;&lt;/span&gt;&lt;span&gt;&lt;/span&gt;&lt;span&gt;=&lt;/span&gt;&lt;span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span&gt;&lt;span&gt;&lt;/span&gt;&lt;span&gt;&lt;span&gt;降序排列后的第&lt;/span&gt;&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;⌊&lt;/span&gt;&lt;span&gt;8640&lt;/span&gt;&lt;span&gt;&lt;/span&gt;&lt;span&gt;×&lt;/span&gt;&lt;span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span&gt;&lt;span&gt;&lt;/span&gt;&lt;span&gt;5%&lt;/span&gt;&lt;span&gt;⌋&lt;/span&gt;&lt;span&gt;&lt;/span&gt;&lt;span&gt;+&lt;/span&gt;&lt;span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span&gt;&lt;span&gt;&lt;/span&gt;&lt;span&gt;1&lt;/span&gt;&lt;span&gt;&lt;/span&gt;&lt;span&gt;=&lt;/span&gt;&lt;span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span&gt;&lt;span&gt;&lt;/span&gt;&lt;span&gt;433&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;&lt;span&gt;个点&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;p&gt;5% 的点相当于一个月里约 &lt;strong&gt;36 个小时&lt;/strong&gt;可以「超标」不算钱。偶发的突发流量，比如一场活动、一次发版，基本都会落在这 5% 里被去掉。模拟数据里，那场直播完全没有影响 95 峰值。&lt;/p&gt;&lt;p&gt;95 峰值对客户和厂商都比较公平：客户的偶发尖峰被容忍了，厂商又能按接近真实容量需求的水平收费。这也是为什么运营商和 CDN 厂商之间、CDN 厂商和大客户之间，绝大多数都用 95 结算。&lt;/p&gt;&lt;p&gt;也正因为 95 这么普遍，才有了「按 95 结算的总成本，怎么分摊给共享带宽的多个客户」这个问题，这就是我那项专利要解决的，见&lt;a href=&quot;/posts/cdn-bandwidth-cost-allocation-patent/&quot;&gt;《我的第一项专利：用「95 区间」把 CDN 带宽成本分得更准》&lt;/a&gt;。&lt;/p&gt;&lt;/section&gt;&lt;section&gt;&lt;h3&gt;3. 日峰值月平均：每天的高峰都要付钱&lt;a href=&quot;#3-日峰值月平均每天的高峰都要付钱&quot;&gt;&lt;span&gt;#&lt;/span&gt;&lt;/a&gt;&lt;/h3&gt;&lt;p&gt;每天取一个峰值，再对一个月的日峰值求平均：&lt;/p&gt;&lt;span&gt;&lt;span&gt;&lt;span&gt;日峰月均=1N∑d=1Nmax⁡(第 d 天的 288 个点)\text{日峰月均} = \frac{1}{N}\sum_{d=1}^{N} \max(\text{第 } d \text{ 天的 288 个点})&lt;/span&gt;&lt;span&gt;&lt;span&gt;&lt;span&gt;&lt;/span&gt;&lt;span&gt;&lt;span&gt;日峰月均&lt;/span&gt;&lt;/span&gt;&lt;span&gt;&lt;/span&gt;&lt;span&gt;=&lt;/span&gt;&lt;span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span&gt;&lt;span&gt;&lt;/span&gt;&lt;span&gt;&lt;span&gt;&lt;/span&gt;&lt;span&gt;&lt;span&gt;&lt;span&gt;&lt;span&gt;&lt;span&gt;&lt;span&gt;&lt;/span&gt;&lt;span&gt;&lt;span&gt;N&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span&gt;&lt;span&gt;&lt;/span&gt;&lt;span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span&gt;&lt;span&gt;&lt;/span&gt;&lt;span&gt;&lt;span&gt;1&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span&gt;​&lt;/span&gt;&lt;/span&gt;&lt;span&gt;&lt;span&gt;&lt;span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span&gt;&lt;/span&gt;&lt;span&gt;&lt;span&gt;&lt;span&gt;&lt;span&gt;&lt;span&gt;&lt;span&gt;&lt;/span&gt;&lt;span&gt;&lt;span&gt;&lt;span&gt;d&lt;/span&gt;&lt;span&gt;=&lt;/span&gt;&lt;span&gt;1&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span&gt;&lt;span&gt;&lt;/span&gt;&lt;span&gt;&lt;span&gt;∑&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span&gt;&lt;span&gt;&lt;/span&gt;&lt;span&gt;&lt;span&gt;&lt;span&gt;N&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span&gt;​&lt;/span&gt;&lt;/span&gt;&lt;span&gt;&lt;span&gt;&lt;span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span&gt;&lt;/span&gt;&lt;span&gt;max&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;&lt;span&gt;第&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;/span&gt;&lt;span&gt;d&lt;/span&gt;&lt;span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;天的&lt;/span&gt;&lt;span&gt; 288 &lt;/span&gt;&lt;span&gt;个点&lt;/span&gt;&lt;/span&gt;&lt;span&gt;)&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;p&gt;它对单日尖峰比月峰值宽容：那场直播只抬高了一天的峰值，被 30 天一平均就摊薄了。但它对「每天都有高峰」的业务不友好，每天晚高峰的那个最高点都被计入。模拟数据里，日峰月均（756）比 95 峰值（678）高出约 11%。&lt;/p&gt;&lt;p&gt;日峰月均更适合&lt;strong&gt;带宽曲线每天都很平稳&lt;/strong&gt;的客户，比如下载、点播类业务；对于有明显晚高峰、偶尔还有活动的业务，95 通常更划算。&lt;/p&gt;&lt;/section&gt;&lt;section&gt;&lt;h3&gt;4. 月第四峰值&lt;a href=&quot;#4-月第四峰值&quot;&gt;&lt;span&gt;#&lt;/span&gt;&lt;/a&gt;&lt;/h3&gt;&lt;p&gt;每天取峰值，然后取一个月里第四大的那个。意思是一个月里可以有 3 天「超标」不算。它介于月峰值和日峰月均之间，一些云厂商把它作为可选的计费方式。&lt;/p&gt;&lt;/section&gt;&lt;section&gt;&lt;h3&gt;5. 夜间带宽减半&lt;a href=&quot;#5-夜间带宽减半&quot;&gt;&lt;span&gt;#&lt;/span&gt;&lt;/a&gt;&lt;/h3&gt;&lt;p&gt;在合同约定的夜间时段（比如凌晨），带宽按一半计入，再参与 95 或其他方式的计算。这是一种鼓励客户把预热、批量下发这类可以错峰的流量挪到夜间的手段，对厂商来说，夜间本来就是带宽的低谷，闲着也是闲着。&lt;/p&gt;&lt;p&gt;要注意的是，它只对&lt;strong&gt;高峰落在夜间&lt;/strong&gt;的客户有意义。模拟客户的高峰在晚上 21 点，夜间本来就不在前 5% 里，所以开不开夜间减半，95 峰值都是 678，没有变化。&lt;/p&gt;&lt;/section&gt;&lt;section&gt;&lt;h3&gt;6. 闲忙时计费&lt;a href=&quot;#6-闲忙时计费&quot;&gt;&lt;span&gt;#&lt;/span&gt;&lt;/a&gt;&lt;/h3&gt;&lt;p&gt;把一天分成忙时和闲时两段，分别计算计费带宽，或者用不同的单价。思路和夜间减半一样：用价格引导客户错峰，让厂商的带宽资源在一天里用得更均匀。&lt;/p&gt;&lt;/section&gt;&lt;/section&gt;
&lt;section&gt;&lt;h2&gt;五、其他计费方式&lt;a href=&quot;#五其他计费方式&quot;&gt;&lt;span&gt;#&lt;/span&gt;&lt;/a&gt;&lt;/h2&gt;&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;请求数计费&lt;/strong&gt;：按请求次数收费，常见于 HTTPS 请求、动态加速、边缘计算等场景，因为这些场景的成本更多花在 CPU（TLS 握手、回源、计算）上，而不是带宽上。在火山 CDN 计费推量里，QPS 计费的客户就是按小时推送请求数据。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;增值服务单独计费&lt;/strong&gt;：比如 HTTPS、WAF、实时日志、图片处理等，一般单独计价，不并入带宽或流量。&lt;/li&gt;
&lt;/ul&gt;&lt;/section&gt;
&lt;section&gt;&lt;h2&gt;六、容易忽略的细节&lt;a href=&quot;#六容易忽略的细节&quot;&gt;&lt;span&gt;#&lt;/span&gt;&lt;/a&gt;&lt;/h2&gt;&lt;p&gt;账单对不上，很多时候不是计费方式本身，而是这些细节：&lt;/p&gt;&lt;p&gt;&lt;strong&gt;1. 计费区域。&lt;/strong&gt; 不同区域的带宽成本差别很大，海外普遍比国内贵，各区域通常&lt;strong&gt;分别计费&lt;/strong&gt;：国内的 95 和海外的 95 各算各的，单价也不同。一个客户的流量往海外迁移，即使总量不变，账单也会上涨。&lt;/p&gt;&lt;p&gt;&lt;strong&gt;2. 包头系数。&lt;/strong&gt; CDN 计费的流量来自节点日志，记录的是应用层（HTTP）的数据量；而实际在网络上传输时，还有 TCP/IP 包头、重传、TLS 握手等开销。所以很多厂商会在日志流量上乘一个大于 1 的&lt;strong&gt;包头系数&lt;/strong&gt;，来近似网络层的真实流量。我在字节做火山 CDN 的计费核对时，包头系数就是独立核算计费值时必须考虑的因素之一。&lt;/p&gt;&lt;p&gt;&lt;strong&gt;3. 统计口径和时间。&lt;/strong&gt; 5 分钟的点是按什么时区切分的？月份按自然月还是账期？有的厂商对 95 的取点规则有细微差别（比如 8640 个点还是按实际天数）。这些在厂商和客户之间对账时，都要事先对齐，否则差几个点就会对不上。&lt;/p&gt;&lt;p&gt;&lt;strong&gt;4. 多个域名、多个项目合并计算。&lt;/strong&gt; 一个客户有多个域名，是先把所有域名的带宽在每个 5 分钟点上加起来，再算 95；还是每个域名各算 95 再相加？两者结果不同：前者因为各域名的高峰可能错开，通常更低。合同里一般约定按客户（或按项目）合并计算。&lt;/p&gt;&lt;/section&gt;
&lt;section&gt;&lt;h2&gt;七、站在两边看：该选哪种计费方式&lt;a href=&quot;#七站在两边看该选哪种计费方式&quot;&gt;&lt;span&gt;#&lt;/span&gt;&lt;/a&gt;&lt;/h2&gt;&lt;p&gt;&lt;strong&gt;站在客户这边&lt;/strong&gt;，选择的关键是&lt;strong&gt;带宽利用率&lt;/strong&gt;，也就是平均带宽和计费带宽的比值：&lt;/p&gt;&lt;span&gt;&lt;span&gt;&lt;span&gt;利用率=月平均带宽计费带宽\text{利用率} = \frac{\text{月平均带宽}}{\text{计费带宽}}&lt;/span&gt;&lt;span&gt;&lt;span&gt;&lt;span&gt;&lt;/span&gt;&lt;span&gt;&lt;span&gt;利用率&lt;/span&gt;&lt;/span&gt;&lt;span&gt;&lt;/span&gt;&lt;span&gt;=&lt;/span&gt;&lt;span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span&gt;&lt;span&gt;&lt;/span&gt;&lt;span&gt;&lt;span&gt;&lt;/span&gt;&lt;span&gt;&lt;span&gt;&lt;span&gt;&lt;span&gt;&lt;span&gt;&lt;span&gt;&lt;/span&gt;&lt;span&gt;&lt;span&gt;&lt;span&gt;计费带宽&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span&gt;&lt;span&gt;&lt;/span&gt;&lt;span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span&gt;&lt;span&gt;&lt;/span&gt;&lt;span&gt;&lt;span&gt;&lt;span&gt;月平均带宽&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span&gt;​&lt;/span&gt;&lt;/span&gt;&lt;span&gt;&lt;span&gt;&lt;span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;p&gt;利用率高，说明带宽曲线平，按带宽买更划算；利用率低，说明大部分时间带宽是闲着的，按流量买更划算。具体的平衡点，可以用前面那个换算来算：&lt;/p&gt;&lt;span&gt;&lt;span&gt;&lt;span&gt;按带宽计费折合的每 GB 单价=带宽单价（元/Mbps/月）324 GB×利用率\text{按带宽计费折合的每 GB 单价} = \frac{\text{带宽单价（元/Mbps/月）}}{324\ \text{GB} \times \text{利用率}}&lt;/span&gt;&lt;span&gt;&lt;span&gt;&lt;span&gt;&lt;/span&gt;&lt;span&gt;&lt;span&gt;按带宽计费折合的每&lt;/span&gt;&lt;span&gt; GB &lt;/span&gt;&lt;span&gt;单价&lt;/span&gt;&lt;/span&gt;&lt;span&gt;&lt;/span&gt;&lt;span&gt;=&lt;/span&gt;&lt;span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span&gt;&lt;span&gt;&lt;/span&gt;&lt;span&gt;&lt;span&gt;&lt;/span&gt;&lt;span&gt;&lt;span&gt;&lt;span&gt;&lt;span&gt;&lt;span&gt;&lt;span&gt;&lt;/span&gt;&lt;span&gt;&lt;span&gt;324&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;&lt;span&gt;GB&lt;/span&gt;&lt;/span&gt;&lt;span&gt;&lt;/span&gt;&lt;span&gt;×&lt;/span&gt;&lt;span&gt;&lt;/span&gt;&lt;span&gt;&lt;span&gt;利用率&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span&gt;&lt;span&gt;&lt;/span&gt;&lt;span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span&gt;&lt;span&gt;&lt;/span&gt;&lt;span&gt;&lt;span&gt;&lt;span&gt;带宽单价（元&lt;/span&gt;&lt;span&gt;/Mbps/&lt;/span&gt;&lt;span&gt;月）&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span&gt;​&lt;/span&gt;&lt;/span&gt;&lt;span&gt;&lt;span&gt;&lt;span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;p&gt;把它和流量单价比一比，就知道哪种更便宜。模拟客户按 95 算的利用率约为 472 ÷ 678 ≈ 70%，曲线很平，按带宽计费大概率更划算。&lt;/p&gt;&lt;p&gt;&lt;strong&gt;站在厂商这边&lt;/strong&gt;，我做账单时最深的体会是：计费方式本质上是&lt;strong&gt;把自己的成本结构翻译给客户&lt;/strong&gt;。厂商向运营商按 95 付费，就倾向于向客户按 95 收费；夜间带宽闲置，就推出夜间减半；HTTPS 消耗 CPU，就按请求数收费。理解了厂商的成本在哪里，就能理解每一种计费方式为什么存在，作为客户也就知道该怎么谈价格了。&lt;/p&gt;&lt;p&gt;95 计费还带来了一个有意思的现象：高峰和大盘错开的客户，接入后几乎不增加厂商的成本，厂商因此可以把同一份带宽卖给多个客户。这就是错峰和超卖，详见&lt;a href=&quot;/posts/cdn-off-peak-and-overselling/&quot;&gt;《CDN 的错峰与超卖》&lt;/a&gt;；接入新客户前怎么据此测算最低价，见&lt;a href=&quot;/posts/cdn-new-customer-pricing/&quot;&gt;《CDN 新客户接入前的价格测算》&lt;/a&gt;。&lt;/p&gt;&lt;p&gt;&lt;strong&gt;站在采购方内部&lt;/strong&gt;，比如字节融合 CDN 这样同时向多家厂商采购、再分给内部业务的平台，还要多解决一个问题：厂商按整体 95 收的钱，怎么公平地分给每个业务、每个域名。单纯取 95 时间点那一刻的占比会失真，这也是我后来设计「95 区间」分摊算法的起点。&lt;/p&gt;&lt;/section&gt;</content:encoded></item><item><title>一个接口管多家 CDN：多厂商刷新预热的适配层设计</title><link>https://tracerzzz.cn/posts/multi-cdn-content-management/</link><guid isPermaLink="true">https://tracerzzz.cn/posts/multi-cdn-content-management/</guid><description>我在字节负责火山多云 CDN 和融合 CDN 的多厂商内容管理（刷新、预热）。这篇讲怎么用适配器模式屏蔽厂商差异，怎么在多云层做配额和限频，怎么按每家厂商的限制拆分任务，怎么用 RocketMQ、Redis、MongoDB 实现分布式下发、共享配额、重试和进度汇总，以及后来如何借助 AI 把新厂商接入时间缩短到一两天。</description><pubDate>Sun, 25 Oct 2026 10:00:00 GMT</pubDate><content:encoded>&lt;p&gt;很多公司会同时用好几家 CDN：容灾、调度、成本都需要。但每家厂商的控制台和 API 都不一样，刷新一批 URL，要分别登录几个控制台、调几套接口。&lt;/p&gt;
&lt;p&gt;我在字节参与过火山引擎多云 CDN 的商业化，负责其中的&lt;strong&gt;内容管理&lt;/strong&gt;模块：客户只调用多云的一套接口，多云平台把刷新、预热任务分发到客户接入的各家厂商，汇总结果后统一返回。同一套能力，对内也服务字节的融合 CDN 平台，两条线并行维护。&lt;/p&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;section&gt;&lt;h2&gt;一、要解决的问题&lt;a href=&quot;#一要解决的问题&quot;&gt;&lt;span&gt;#&lt;/span&gt;&lt;/a&gt;&lt;/h2&gt;&lt;p&gt;内容管理看起来简单，就是把一批 URL 发给厂商。难点在于&lt;strong&gt;各家厂商的差异和限制&lt;/strong&gt;：&lt;/p&gt;&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;接口不同&lt;/strong&gt;：认证方式（AK/SK 签名、Token、各种自定义签名算法）、入参出参格式、任务状态的定义都不一样；&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;单次提交上限不同&lt;/strong&gt;：有的厂商一次最多提交几十个 URL，有的上百个；&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;频率限制不同&lt;/strong&gt;：每秒能调用几次、超了是直接报错还是排队，各不相同；&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;总量限制不同&lt;/strong&gt;：很多厂商对每个账号每天能刷新、预热的 URL 总数有配额；&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;都是异步的&lt;/strong&gt;：提交成功只代表厂商收到了任务，真正执行完要另外查询。&lt;/li&gt;
&lt;/ul&gt;&lt;p&gt;多云平台要做的，就是把这些差异全部吸收掉，让客户感觉只在用「一家」CDN。&lt;/p&gt;&lt;/section&gt;
&lt;section&gt;&lt;h2&gt;二、整体架构&lt;a href=&quot;#二整体架构&quot;&gt;&lt;span&gt;#&lt;/span&gt;&lt;/a&gt;&lt;/h2&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;&lt;p&gt;统一接口&lt;br /&gt;&lt;br /&gt;单次 ≤ 1000 URL，50 QPS&lt;/p&gt;&lt;/span&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;&lt;p&gt;主任务&lt;/p&gt;&lt;/span&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;&lt;p&gt;按厂商拆分的子任务&lt;/p&gt;&lt;/span&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;&lt;/span&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;&lt;/span&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;&lt;/span&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;&lt;p&gt;共享各厂商配额&lt;/p&gt;&lt;/span&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;&lt;p&gt;共享各厂商配额&lt;/p&gt;&lt;/span&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;&lt;p&gt;共享各厂商配额&lt;/p&gt;&lt;/span&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;&lt;/span&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;&lt;/span&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;&lt;/span&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;&lt;/span&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;&lt;/span&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;&lt;/span&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;&lt;p&gt;查询执行状态&lt;/p&gt;&lt;/span&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;&lt;p&gt;更新子任务、主任务进度&lt;/p&gt;&lt;/span&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;&lt;p&gt;客户 / 融合 CDN&lt;/p&gt;&lt;/span&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;&lt;p&gt;多云内容管理 API&lt;/p&gt;&lt;/span&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;&lt;p&gt;MongoDB&lt;br /&gt;&lt;br /&gt;主任务 / 子任务&lt;/p&gt;&lt;/span&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;&lt;p&gt;RocketMQ&lt;/p&gt;&lt;/span&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;&lt;p&gt;下发服务 1&lt;/p&gt;&lt;/span&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;&lt;p&gt;下发服务 2&lt;/p&gt;&lt;/span&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;&lt;p&gt;下发服务 N&lt;/p&gt;&lt;/span&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;&lt;p&gt;Redis&lt;/p&gt;&lt;/span&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;&lt;p&gt;厂商适配层&lt;/p&gt;&lt;/span&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;&lt;p&gt;厂商 A&lt;/p&gt;&lt;/span&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;&lt;p&gt;厂商 B&lt;/p&gt;&lt;/span&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;&lt;p&gt;厂商 C&lt;/p&gt;&lt;/span&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;&lt;p&gt;定时轮询&lt;/p&gt;&lt;/span&gt;&lt;/div&gt;
&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;&lt;p&gt;统一接口&lt;br /&gt;&lt;br /&gt;单次 ≤ 1000 URL，50 QPS&lt;/p&gt;&lt;/span&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;&lt;p&gt;主任务&lt;/p&gt;&lt;/span&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;&lt;p&gt;按厂商拆分的子任务&lt;/p&gt;&lt;/span&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;&lt;/span&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;&lt;/span&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;&lt;/span&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;&lt;p&gt;共享各厂商配额&lt;/p&gt;&lt;/span&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;&lt;p&gt;共享各厂商配额&lt;/p&gt;&lt;/span&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;&lt;p&gt;共享各厂商配额&lt;/p&gt;&lt;/span&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;&lt;/span&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;&lt;/span&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;&lt;/span&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;&lt;/span&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;&lt;/span&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;&lt;/span&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;&lt;p&gt;查询执行状态&lt;/p&gt;&lt;/span&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;&lt;p&gt;更新子任务、主任务进度&lt;/p&gt;&lt;/span&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;&lt;p&gt;客户 / 融合 CDN&lt;/p&gt;&lt;/span&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;&lt;p&gt;多云内容管理 API&lt;/p&gt;&lt;/span&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;&lt;p&gt;MongoDB&lt;br /&gt;&lt;br /&gt;主任务 / 子任务&lt;/p&gt;&lt;/span&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;&lt;p&gt;RocketMQ&lt;/p&gt;&lt;/span&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;&lt;p&gt;下发服务 1&lt;/p&gt;&lt;/span&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;&lt;p&gt;下发服务 2&lt;/p&gt;&lt;/span&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;&lt;p&gt;下发服务 N&lt;/p&gt;&lt;/span&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;&lt;p&gt;Redis&lt;/p&gt;&lt;/span&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;&lt;p&gt;厂商适配层&lt;/p&gt;&lt;/span&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;&lt;p&gt;厂商 A&lt;/p&gt;&lt;/span&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;&lt;p&gt;厂商 B&lt;/p&gt;&lt;/span&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;&lt;p&gt;厂商 C&lt;/p&gt;&lt;/span&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;&lt;p&gt;定时轮询&lt;/p&gt;&lt;/span&gt;&lt;/div&gt;
&lt;/div&gt;&lt;/div&gt;&lt;/div&gt;&lt;p&gt;一次刷新请求的生命周期：&lt;/p&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;定时轮询厂商（经适配器）Redis下发服务RocketMQMongoDB多云 API客户定时轮询厂商（经适配器）Redis下发服务RocketMQMongoDB多云 API客户alt[配额充足][配额不足]loop[每个主任务定时执行，直到全部子任务结束]提交刷新（≤ 1000 URL）1多云层配额、限频校验2创建主任务3按厂商和单次上限拆出子任务4投递子任务5返回主任务 ID6消费子任务7检查并扣减该厂商配额8提交 URL9厂商任务 ID10子任务 → 已提交11延迟后重新投递12查询子任务执行状态13更新子任务，汇总主任务状态和完成率14查询主任务进度15
&lt;/div&gt;&lt;div&gt;定时轮询厂商（经适配器）Redis下发服务RocketMQMongoDB多云 API客户定时轮询厂商（经适配器）Redis下发服务RocketMQMongoDB多云 API客户alt[配额充足][配额不足]loop[每个主任务定时执行，直到全部子任务结束]提交刷新（≤ 1000 URL）1多云层配额、限频校验2创建主任务3按厂商和单次上限拆出子任务4投递子任务5返回主任务 ID6消费子任务7检查并扣减该厂商配额8提交 URL9厂商任务 ID10子任务 → 已提交11延迟后重新投递12查询子任务执行状态13更新子任务，汇总主任务状态和完成率14查询主任务进度15
&lt;/div&gt;&lt;/div&gt;&lt;/div&gt;&lt;ol&gt;
&lt;li&gt;客户调用多云接口提交 URL，多云层先做&lt;strong&gt;自己的配额和限频&lt;/strong&gt;；&lt;/li&gt;
&lt;li&gt;生成一个&lt;strong&gt;主任务&lt;/strong&gt;，存进 MongoDB；&lt;/li&gt;
&lt;li&gt;按客户接入的厂商、以及每家厂商的单次上限，把主任务拆成多个&lt;strong&gt;子任务&lt;/strong&gt;，投递到 RocketMQ 排队；&lt;/li&gt;
&lt;li&gt;多台下发服务消费子任务，通过 Redis 检查并扣减该厂商的&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;p&gt;下面逐个展开。&lt;/p&gt;&lt;/section&gt;
&lt;section&gt;&lt;h2&gt;三、适配器模式：屏蔽厂商差异&lt;a href=&quot;#三适配器模式屏蔽厂商差异&quot;&gt;&lt;span&gt;#&lt;/span&gt;&lt;/a&gt;&lt;/h2&gt;&lt;p&gt;所有厂商实现同一个接口，多云平台的其他部分只和这个接口打交道，完全不知道背后是哪家厂商：&lt;/p&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;&lt;figure&gt;&lt;figcaption&gt;&lt;/figcaption&gt;&lt;pre&gt;&lt;code&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;1&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;// 统一的任务类型和状态，由多云平台定义&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;2&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;type&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;TaskType&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;string&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;// refresh_url / refresh_dir / preload&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;3&lt;/div&gt;&lt;/div&gt;&lt;div&gt;
&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;4&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;type&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;VendorTaskStatus&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;string&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;// pending / running / success / failed&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;5&lt;/div&gt;&lt;/div&gt;&lt;div&gt;
&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;6&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;// 每家厂商的能力和限制，作为适配器的一部分声明出来&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;7&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;type&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;Limits&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;struct&lt;/span&gt;&lt;span&gt; {&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;8&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;  &lt;/span&gt;&lt;span&gt;MaxURLsPerCall&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;int&lt;/span&gt;&lt;span&gt;           &lt;/span&gt;&lt;span&gt;// 单次提交上限&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;9&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;  &lt;/span&gt;&lt;span&gt;CallsPerSecond&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;int&lt;/span&gt;&lt;span&gt;           &lt;/span&gt;&lt;span&gt;// 接口频率限制&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;10&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;  &lt;/span&gt;&lt;span&gt;DailyQuota&lt;/span&gt;&lt;span&gt;     &lt;/span&gt;&lt;span&gt;map&lt;/span&gt;&lt;span&gt;[&lt;/span&gt;&lt;span&gt;TaskType&lt;/span&gt;&lt;span&gt;]&lt;/span&gt;&lt;span&gt;int&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;// 每日总量配额，0 表示不限&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;11&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;}&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;12&lt;/div&gt;&lt;/div&gt;&lt;div&gt;
&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;13&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;type&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;Adapter&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;interface&lt;/span&gt;&lt;span&gt; {&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;14&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;  &lt;/span&gt;&lt;span&gt;Name&lt;/span&gt;&lt;span&gt;() &lt;/span&gt;&lt;span&gt;string&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;15&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;  &lt;/span&gt;&lt;span&gt;Limits&lt;/span&gt;&lt;span&gt;() &lt;/span&gt;&lt;span&gt;Limits&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;16&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;  &lt;/span&gt;&lt;span&gt;// 提交任务，返回厂商侧的任务 ID&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;17&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;  &lt;/span&gt;&lt;span&gt;Submit&lt;/span&gt;&lt;span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;ctx&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;/span&gt;&lt;span&gt;context&lt;/span&gt;&lt;span&gt;.&lt;/span&gt;&lt;span&gt;Context&lt;/span&gt;&lt;span&gt;&lt;span&gt;, &lt;/span&gt;&lt;span&gt;cred&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;/span&gt;&lt;span&gt;Credential&lt;/span&gt;&lt;span&gt;&lt;span&gt;, &lt;/span&gt;&lt;span&gt;typ&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;/span&gt;&lt;span&gt;TaskType&lt;/span&gt;&lt;span&gt;&lt;span&gt;, &lt;/span&gt;&lt;span&gt;urls&lt;/span&gt;&lt;span&gt; []&lt;/span&gt;&lt;/span&gt;&lt;span&gt;string&lt;/span&gt;&lt;span&gt;&lt;span&gt;) (&lt;/span&gt;&lt;span&gt;vendorTaskID&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;/span&gt;&lt;span&gt;string&lt;/span&gt;&lt;span&gt;&lt;span&gt;, &lt;/span&gt;&lt;span&gt;err&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;/span&gt;&lt;span&gt;error&lt;/span&gt;&lt;span&gt;)&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;18&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;  &lt;/span&gt;&lt;span&gt;// 查询执行状态，把厂商自己的状态翻译成统一状态&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;19&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;  &lt;/span&gt;&lt;span&gt;Query&lt;/span&gt;&lt;span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;ctx&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;/span&gt;&lt;span&gt;context&lt;/span&gt;&lt;span&gt;.&lt;/span&gt;&lt;span&gt;Context&lt;/span&gt;&lt;span&gt;&lt;span&gt;, &lt;/span&gt;&lt;span&gt;cred&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;/span&gt;&lt;span&gt;Credential&lt;/span&gt;&lt;span&gt;&lt;span&gt;, &lt;/span&gt;&lt;span&gt;vendorTaskID&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;/span&gt;&lt;span&gt;string&lt;/span&gt;&lt;span&gt;) (&lt;/span&gt;&lt;span&gt;VendorTaskStatus&lt;/span&gt;&lt;span&gt;, &lt;/span&gt;&lt;span&gt;error&lt;/span&gt;&lt;span&gt;)&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;20&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;}&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;21&lt;/div&gt;&lt;/div&gt;&lt;div&gt;
&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;22&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;var&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;registry&lt;/span&gt;&lt;span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;=&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;/span&gt;&lt;span&gt;map&lt;/span&gt;&lt;span&gt;[&lt;/span&gt;&lt;span&gt;string&lt;/span&gt;&lt;span&gt;]&lt;/span&gt;&lt;span&gt;Adapter&lt;/span&gt;&lt;span&gt;{}&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;23&lt;/div&gt;&lt;/div&gt;&lt;div&gt;
&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;24&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;func&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;Register&lt;/span&gt;&lt;span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;a&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;/span&gt;&lt;span&gt;Adapter&lt;/span&gt;&lt;span&gt;) { &lt;/span&gt;&lt;span&gt;registry&lt;/span&gt;&lt;span&gt;[&lt;/span&gt;&lt;span&gt;a&lt;/span&gt;&lt;span&gt;.&lt;/span&gt;&lt;span&gt;Name&lt;/span&gt;&lt;span&gt;&lt;span&gt;()] &lt;/span&gt;&lt;span&gt;=&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;/span&gt;&lt;span&gt;a&lt;/span&gt;&lt;span&gt; }&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;/code&gt;&lt;/pre&gt;&lt;div&gt;&lt;div&gt;&lt;/div&gt;&lt;div&gt;&lt;/div&gt;&lt;/div&gt;&lt;/figure&gt;&lt;div&gt;&lt;/div&gt;&lt;/div&gt;&lt;span&gt;展开&lt;/span&gt;&lt;span&gt;收起&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;p&gt;每家厂商一个实现，认证签名、参数拼装、状态翻译都封装在里面。接入一家新厂商，就是新写一个适配器并注册进去，核心流程一行都不用改。&lt;/p&gt;&lt;p&gt;&lt;strong&gt;限制也是适配器的一部分&lt;/strong&gt;。单次上限、频率、配额这些信息跟着适配器走，拆分和限流的逻辑读取它们，而不是写死在流程里。这样不同厂商的差异就收敛在一个地方。&lt;/p&gt;&lt;/section&gt;
&lt;section&gt;&lt;h2&gt;四、多云层的配额与限频&lt;a href=&quot;#四多云层的配额与限频&quot;&gt;&lt;span&gt;#&lt;/span&gt;&lt;/a&gt;&lt;/h2&gt;&lt;p&gt;客户面对的是多云平台，所以多云层先要有一套&lt;strong&gt;自己的、统一的限制&lt;/strong&gt;：&lt;/p&gt;&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;单次最多 1000 个 URL&lt;/strong&gt;；&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;接口频率 50 QPS&lt;/strong&gt;，也就是每秒最多可以接收 5 万个 URL。&lt;/li&gt;
&lt;/ul&gt;&lt;p&gt;这组数值是参考各家主流厂商的能力，取的一个中间值。它带来两种情况：&lt;/p&gt;&lt;ul&gt;
&lt;li&gt;如果多云的单次上限比某家厂商的大，提交给这家厂商时要&lt;strong&gt;拆分&lt;/strong&gt;成多批；&lt;/li&gt;
&lt;li&gt;如果比厂商的小，就直接提交。&lt;/li&gt;
&lt;/ul&gt;&lt;p&gt;这样设计的好处是，客户不需要了解每家厂商的限制；代价是多云层要负责把一个「大请求」翻译成符合每家厂商规矩的若干「小请求」。&lt;/p&gt;&lt;p&gt;另外，如果厂商侧有&lt;strong&gt;每日总量限制&lt;/strong&gt;，这是多云平台无法替厂商突破的。这类限制会同步给客户，让客户去厂商那里申请提高限额。&lt;/p&gt;&lt;/section&gt;
&lt;section&gt;&lt;h2&gt;五、任务拆分与排队：RocketMQ&lt;a href=&quot;#五任务拆分与排队rocketmq&quot;&gt;&lt;span&gt;#&lt;/span&gt;&lt;/a&gt;&lt;/h2&gt;&lt;p&gt;客户的请求是突发的，比如一次发版要刷新几万个 URL；而厂商接口的承受能力是固定的。中间必须有一层缓冲，所以子任务会进 &lt;strong&gt;RocketMQ&lt;/strong&gt; 排队，由多台下发服务按各厂商的节奏消费。&lt;/p&gt;&lt;p&gt;拆分逻辑按每家厂商的单次上限切批：&lt;/p&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;&lt;figure&gt;&lt;figcaption&gt;&lt;/figcaption&gt;&lt;pre&gt;&lt;code&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;1&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;func&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;split&lt;/span&gt;&lt;span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;urls&lt;/span&gt;&lt;span&gt; []&lt;/span&gt;&lt;/span&gt;&lt;span&gt;string&lt;/span&gt;&lt;span&gt;&lt;span&gt;, &lt;/span&gt;&lt;span&gt;limit&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;/span&gt;&lt;span&gt;int&lt;/span&gt;&lt;span&gt;) [][]&lt;/span&gt;&lt;span&gt;string&lt;/span&gt;&lt;span&gt; {&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;2&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;  &lt;/span&gt;&lt;span&gt;var&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;batches&lt;/span&gt;&lt;span&gt; [][]&lt;/span&gt;&lt;span&gt;string&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;3&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;  &lt;/span&gt;&lt;span&gt;for&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;len&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;urls&lt;/span&gt;&lt;span&gt;&lt;span&gt;) &lt;/span&gt;&lt;span&gt;&amp;gt;&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;/span&gt;&lt;span&gt;limit&lt;/span&gt;&lt;span&gt; {&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;4&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;    &lt;/span&gt;&lt;span&gt;batches&lt;/span&gt;&lt;span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;=&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;/span&gt;&lt;span&gt;append&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;batches&lt;/span&gt;&lt;span&gt;, &lt;/span&gt;&lt;span&gt;urls&lt;/span&gt;&lt;span&gt;[:&lt;/span&gt;&lt;span&gt;limit&lt;/span&gt;&lt;span&gt;])&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;5&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;    &lt;/span&gt;&lt;span&gt;urls&lt;/span&gt;&lt;span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;=&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;/span&gt;&lt;span&gt;urls&lt;/span&gt;&lt;span&gt;[&lt;/span&gt;&lt;span&gt;limit&lt;/span&gt;&lt;span&gt;:]&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;6&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;&lt;span&gt;  &lt;/span&gt;&lt;/span&gt;&lt;span&gt;}&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;7&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;  &lt;/span&gt;&lt;span&gt;if&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;len&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;urls&lt;/span&gt;&lt;span&gt;&lt;span&gt;) &lt;/span&gt;&lt;span&gt;&amp;gt;&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;/span&gt;&lt;span&gt;0&lt;/span&gt;&lt;span&gt; {&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;8&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;    &lt;/span&gt;&lt;span&gt;batches&lt;/span&gt;&lt;span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;=&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;/span&gt;&lt;span&gt;append&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;batches&lt;/span&gt;&lt;span&gt;, &lt;/span&gt;&lt;span&gt;urls&lt;/span&gt;&lt;span&gt;)&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;9&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;&lt;span&gt;  &lt;/span&gt;&lt;/span&gt;&lt;span&gt;}&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;10&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;  &lt;/span&gt;&lt;span&gt;return&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;batches&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;11&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;}&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;12&lt;/div&gt;&lt;/div&gt;&lt;div&gt;
&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;13&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;// 一个主任务，按客户接入的每家厂商分别拆成子任务&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;14&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;func&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;dispatch&lt;/span&gt;&lt;span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;task&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;*&lt;/span&gt;&lt;/span&gt;&lt;span&gt;MainTask&lt;/span&gt;&lt;span&gt;) &lt;/span&gt;&lt;span&gt;error&lt;/span&gt;&lt;span&gt; {&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;15&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;  &lt;/span&gt;&lt;span&gt;for&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;_&lt;/span&gt;&lt;span&gt;, &lt;/span&gt;&lt;span&gt;vendor&lt;/span&gt;&lt;span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;:=&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;/span&gt;&lt;span&gt;range&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;task&lt;/span&gt;&lt;span&gt;.&lt;/span&gt;&lt;span&gt;Vendors&lt;/span&gt;&lt;span&gt; {&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;16&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;    &lt;/span&gt;&lt;span&gt;a&lt;/span&gt;&lt;span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;:=&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;/span&gt;&lt;span&gt;registry&lt;/span&gt;&lt;span&gt;[&lt;/span&gt;&lt;span&gt;vendor&lt;/span&gt;&lt;span&gt;]&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;17&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;    &lt;/span&gt;&lt;span&gt;for&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;_&lt;/span&gt;&lt;span&gt;, &lt;/span&gt;&lt;span&gt;batch&lt;/span&gt;&lt;span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;:=&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;/span&gt;&lt;span&gt;range&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;split&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;task&lt;/span&gt;&lt;span&gt;.&lt;/span&gt;&lt;span&gt;URLs&lt;/span&gt;&lt;span&gt;, &lt;/span&gt;&lt;span&gt;a&lt;/span&gt;&lt;span&gt;.&lt;/span&gt;&lt;span&gt;Limits&lt;/span&gt;&lt;span&gt;().&lt;/span&gt;&lt;span&gt;MaxURLsPerCall&lt;/span&gt;&lt;span&gt;) {&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;18&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;      &lt;/span&gt;&lt;span&gt;sub&lt;/span&gt;&lt;span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;:=&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;/span&gt;&lt;span&gt;NewSubTask&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;task&lt;/span&gt;&lt;span&gt;.&lt;/span&gt;&lt;span&gt;ID&lt;/span&gt;&lt;span&gt;, &lt;/span&gt;&lt;span&gt;vendor&lt;/span&gt;&lt;span&gt;, &lt;/span&gt;&lt;span&gt;batch&lt;/span&gt;&lt;span&gt;)&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;19&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;      &lt;/span&gt;&lt;span&gt;if&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;err&lt;/span&gt;&lt;span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;:=&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;/span&gt;&lt;span&gt;saveSubTask&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;sub&lt;/span&gt;&lt;span&gt;); &lt;/span&gt;&lt;span&gt;err&lt;/span&gt;&lt;span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;!=&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;/span&gt;&lt;span&gt;nil&lt;/span&gt;&lt;span&gt; { &lt;/span&gt;&lt;span&gt;// 子任务先落库，再投递&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;20&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;        &lt;/span&gt;&lt;span&gt;return&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;err&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;21&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;&lt;span&gt;      &lt;/span&gt;&lt;/span&gt;&lt;span&gt;}&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;22&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;      &lt;/span&gt;&lt;span&gt;if&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;err&lt;/span&gt;&lt;span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;:=&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;/span&gt;&lt;span&gt;mq&lt;/span&gt;&lt;span&gt;.&lt;/span&gt;&lt;span&gt;Send&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;&quot;content-task&quot;&lt;/span&gt;&lt;span&gt;, &lt;/span&gt;&lt;span&gt;sub&lt;/span&gt;&lt;span&gt;.&lt;/span&gt;&lt;span&gt;ID&lt;/span&gt;&lt;span&gt;); &lt;/span&gt;&lt;span&gt;err&lt;/span&gt;&lt;span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;!=&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;/span&gt;&lt;span&gt;nil&lt;/span&gt;&lt;span&gt; {&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;23&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;        &lt;/span&gt;&lt;span&gt;return&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;err&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;24&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;&lt;span&gt;      &lt;/span&gt;&lt;/span&gt;&lt;span&gt;}&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;25&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;&lt;span&gt;    &lt;/span&gt;&lt;/span&gt;&lt;span&gt;}&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;26&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;&lt;span&gt;  &lt;/span&gt;&lt;/span&gt;&lt;span&gt;}&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;27&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;  &lt;/span&gt;&lt;span&gt;return&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;nil&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;28&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;}&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;/code&gt;&lt;/pre&gt;&lt;div&gt;&lt;div&gt;&lt;/div&gt;&lt;div&gt;&lt;/div&gt;&lt;/div&gt;&lt;/figure&gt;&lt;div&gt;&lt;/div&gt;&lt;/div&gt;&lt;span&gt;展开&lt;/span&gt;&lt;span&gt;收起&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;p&gt;实际的拆分比这复杂一些：有的厂商对目录刷新、URL 刷新、预热分别有不同上限，有的对单个任务里的域名数量也有要求，所以每家厂商有自己单独的拆分逻辑。这部分只能靠深入了解每家厂商的机制，再一点点适配。&lt;/p&gt;&lt;p&gt;主任务和子任务存在 &lt;strong&gt;MongoDB&lt;/strong&gt; 里：一个主任务下挂着数量不定的子任务，每家厂商返回的任务信息格式也不一样，文档型数据库存这种结构很自然。&lt;/p&gt;&lt;/section&gt;
&lt;section&gt;&lt;h2&gt;六、分布式共享配额：Redis&lt;a href=&quot;#六分布式共享配额redis&quot;&gt;&lt;span&gt;#&lt;/span&gt;&lt;/a&gt;&lt;/h2&gt;&lt;p&gt;下发服务有多台，它们同时在给同一家厂商提交任务。厂商的频率和总量限制是针对&lt;strong&gt;整个账号&lt;/strong&gt;的，不是针对某一台机器的。如果每台服务器各自计数，加起来就会超限。&lt;/p&gt;&lt;p&gt;所以配额计数放在 &lt;strong&gt;Redis&lt;/strong&gt; 里，所有下发服务共享。Redis 的键可以设置过期时间，&lt;strong&gt;正好对应厂商限额的时间窗口&lt;/strong&gt;：限每秒就让键 1 秒过期，限每天就让键在当天结束时过期，窗口一过，计数自动清零。&lt;/p&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;&lt;p&gt;Redis：厂商 A 的配额计数&lt;/p&gt;&lt;/span&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;&lt;p&gt;多台下发服务（同时给厂商 A 提交）&lt;/p&gt;&lt;/span&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;&lt;p&gt;INCRBY 原子计数&lt;/p&gt;&lt;/span&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;&lt;p&gt;INCRBY 原子计数&lt;/p&gt;&lt;/span&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;&lt;p&gt;INCRBY 原子计数&lt;/p&gt;&lt;/span&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;&lt;p&gt;未超限：放行&lt;/p&gt;&lt;/span&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;&lt;p&gt;超限：DECRBY 回退&lt;/p&gt;&lt;/span&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;&lt;p&gt;窗口结束，键过期&lt;/p&gt;&lt;/span&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;&lt;p&gt;下发服务 1&lt;br /&gt;&lt;br /&gt;本批 100 个&lt;/p&gt;&lt;/span&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;&lt;p&gt;下发服务 2&lt;br /&gt;&lt;br /&gt;本批 100 个&lt;/p&gt;&lt;/span&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;&lt;p&gt;下发服务 3&lt;br /&gt;&lt;br /&gt;本批 100 个&lt;/p&gt;&lt;/span&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;&lt;p&gt;quota:A:账号:当前窗口&lt;br /&gt;&lt;br /&gt;已用 / 上限&lt;br /&gt;&lt;br /&gt;TTL = 窗口剩余时间&lt;/p&gt;&lt;/span&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;&lt;p&gt;调用厂商 A 接口&lt;/p&gt;&lt;/span&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;&lt;p&gt;子任务延迟重投&lt;/p&gt;&lt;/span&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;&lt;p&gt;计数自动清零&lt;/p&gt;&lt;/span&gt;&lt;/div&gt;
&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;&lt;p&gt;Redis：厂商 A 的配额计数&lt;/p&gt;&lt;/span&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;&lt;p&gt;多台下发服务（同时给厂商 A 提交）&lt;/p&gt;&lt;/span&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;&lt;p&gt;INCRBY 原子计数&lt;/p&gt;&lt;/span&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;&lt;p&gt;INCRBY 原子计数&lt;/p&gt;&lt;/span&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;&lt;p&gt;INCRBY 原子计数&lt;/p&gt;&lt;/span&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;&lt;p&gt;未超限：放行&lt;/p&gt;&lt;/span&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;&lt;p&gt;超限：DECRBY 回退&lt;/p&gt;&lt;/span&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;&lt;p&gt;窗口结束，键过期&lt;/p&gt;&lt;/span&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;&lt;p&gt;下发服务 1&lt;br /&gt;&lt;br /&gt;本批 100 个&lt;/p&gt;&lt;/span&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;&lt;p&gt;下发服务 2&lt;br /&gt;&lt;br /&gt;本批 100 个&lt;/p&gt;&lt;/span&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;&lt;p&gt;下发服务 3&lt;br /&gt;&lt;br /&gt;本批 100 个&lt;/p&gt;&lt;/span&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;&lt;p&gt;quota:A:账号:当前窗口&lt;br /&gt;&lt;br /&gt;已用 / 上限&lt;br /&gt;&lt;br /&gt;TTL = 窗口剩余时间&lt;/p&gt;&lt;/span&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;&lt;p&gt;调用厂商 A 接口&lt;/p&gt;&lt;/span&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;&lt;p&gt;子任务延迟重投&lt;/p&gt;&lt;/span&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;&lt;p&gt;计数自动清零&lt;/p&gt;&lt;/span&gt;&lt;/div&gt;
&lt;/div&gt;&lt;/div&gt;&lt;/div&gt;&lt;p&gt;检查和扣减必须避免竞争：如果先 &lt;code&gt;GET&lt;/code&gt; 读出已用量、判断够不够、再写回去，两台服务器可能同时读到「还剩 100」，都去提交 100 个，就超了。&lt;/p&gt;&lt;p&gt;解决办法是利用 &lt;strong&gt;&lt;code&gt;INCRBY&lt;/code&gt; 本身是原子的&lt;/strong&gt;：不先读再写，而是&lt;strong&gt;先加、再判断&lt;/strong&gt;。Redis 单线程执行命令，多台服务并发 &lt;code&gt;INCRBY&lt;/code&gt; 时，每台拿到的都是自己加完之后的结果，不会读到同一个值。加完发现超过上限，就 &lt;code&gt;DECRBY&lt;/code&gt; 把这次的数量退回去，本次不放行。&lt;/p&gt;&lt;p&gt;下发服务用 Go（go-redis）直接操作 Redis，大致是这样（示例）：&lt;/p&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;&lt;figure&gt;&lt;figcaption&gt;&lt;/figcaption&gt;&lt;pre&gt;&lt;code&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;1&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;// 申请本次要提交的 n 个 URL 的配额；limit 为该厂商在窗口内的上限&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;2&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;func&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;acquire&lt;/span&gt;&lt;span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;ctx&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;/span&gt;&lt;span&gt;context&lt;/span&gt;&lt;span&gt;.&lt;/span&gt;&lt;span&gt;Context&lt;/span&gt;&lt;span&gt;&lt;span&gt;, &lt;/span&gt;&lt;span&gt;rdb&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;*&lt;/span&gt;&lt;/span&gt;&lt;span&gt;redis&lt;/span&gt;&lt;span&gt;.&lt;/span&gt;&lt;span&gt;Client&lt;/span&gt;&lt;span&gt;&lt;span&gt;, &lt;/span&gt;&lt;span&gt;vendor&lt;/span&gt;&lt;span&gt;, &lt;/span&gt;&lt;span&gt;account&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;/span&gt;&lt;span&gt;string&lt;/span&gt;&lt;span&gt;,&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;3&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;&lt;span&gt;  &lt;/span&gt;&lt;/span&gt;&lt;span&gt;n&lt;/span&gt;&lt;span&gt;, &lt;/span&gt;&lt;span&gt;limit&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;int64&lt;/span&gt;&lt;span&gt;&lt;span&gt;, &lt;/span&gt;&lt;span&gt;window&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;/span&gt;&lt;span&gt;time&lt;/span&gt;&lt;span&gt;.&lt;/span&gt;&lt;span&gt;Duration&lt;/span&gt;&lt;span&gt;) (&lt;/span&gt;&lt;span&gt;bool&lt;/span&gt;&lt;span&gt;, &lt;/span&gt;&lt;span&gt;error&lt;/span&gt;&lt;span&gt;) {&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;4&lt;/div&gt;&lt;/div&gt;&lt;div&gt;
&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;5&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;  &lt;/span&gt;&lt;span&gt;// 键名带上窗口编号，窗口切换时自然换成新键&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;6&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;  &lt;/span&gt;&lt;span&gt;key&lt;/span&gt;&lt;span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;:=&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;/span&gt;&lt;span&gt;fmt&lt;/span&gt;&lt;span&gt;.&lt;/span&gt;&lt;span&gt;Sprintf&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;&quot;quota:&lt;/span&gt;&lt;span&gt;%s&lt;/span&gt;&lt;span&gt;:&lt;/span&gt;&lt;span&gt;%s&lt;/span&gt;&lt;span&gt;:&lt;/span&gt;&lt;span&gt;%d&lt;/span&gt;&lt;span&gt;&quot;&lt;/span&gt;&lt;span&gt;, &lt;/span&gt;&lt;span&gt;vendor&lt;/span&gt;&lt;span&gt;, &lt;/span&gt;&lt;span&gt;account&lt;/span&gt;&lt;span&gt;,&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;7&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;    &lt;/span&gt;&lt;span&gt;time&lt;/span&gt;&lt;span&gt;.&lt;/span&gt;&lt;span&gt;Now&lt;/span&gt;&lt;span&gt;().&lt;/span&gt;&lt;span&gt;Unix&lt;/span&gt;&lt;span&gt;&lt;span&gt;()&lt;/span&gt;&lt;span&gt;/&lt;/span&gt;&lt;/span&gt;&lt;span&gt;int64&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;window&lt;/span&gt;&lt;span&gt;.&lt;/span&gt;&lt;span&gt;Seconds&lt;/span&gt;&lt;span&gt;()))&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;8&lt;/div&gt;&lt;/div&gt;&lt;div&gt;
&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;9&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;  &lt;/span&gt;&lt;span&gt;// 窗口内第一次使用时创建键并设置过期时间；键已存在则什么都不做&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;10&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;  &lt;/span&gt;&lt;span&gt;if&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;err&lt;/span&gt;&lt;span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;:=&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;/span&gt;&lt;span&gt;rdb&lt;/span&gt;&lt;span&gt;.&lt;/span&gt;&lt;span&gt;SetNX&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;ctx&lt;/span&gt;&lt;span&gt;, &lt;/span&gt;&lt;span&gt;key&lt;/span&gt;&lt;span&gt;, &lt;/span&gt;&lt;span&gt;0&lt;/span&gt;&lt;span&gt;, &lt;/span&gt;&lt;span&gt;window&lt;/span&gt;&lt;span&gt;).&lt;/span&gt;&lt;span&gt;Err&lt;/span&gt;&lt;span&gt;(); &lt;/span&gt;&lt;span&gt;err&lt;/span&gt;&lt;span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;!=&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;/span&gt;&lt;span&gt;nil&lt;/span&gt;&lt;span&gt; {&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;11&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;    &lt;/span&gt;&lt;span&gt;return&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;false&lt;/span&gt;&lt;span&gt;, &lt;/span&gt;&lt;span&gt;err&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;12&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;&lt;span&gt;  &lt;/span&gt;&lt;/span&gt;&lt;span&gt;}&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;13&lt;/div&gt;&lt;/div&gt;&lt;div&gt;
&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;14&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;  &lt;/span&gt;&lt;span&gt;// 原子地加上本次数量，返回加完之后的总量&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;15&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;  &lt;/span&gt;&lt;span&gt;used&lt;/span&gt;&lt;span&gt;, &lt;/span&gt;&lt;span&gt;err&lt;/span&gt;&lt;span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;:=&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;/span&gt;&lt;span&gt;rdb&lt;/span&gt;&lt;span&gt;.&lt;/span&gt;&lt;span&gt;IncrBy&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;ctx&lt;/span&gt;&lt;span&gt;, &lt;/span&gt;&lt;span&gt;key&lt;/span&gt;&lt;span&gt;, &lt;/span&gt;&lt;span&gt;n&lt;/span&gt;&lt;span&gt;).&lt;/span&gt;&lt;span&gt;Result&lt;/span&gt;&lt;span&gt;()&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;16&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;  &lt;/span&gt;&lt;span&gt;if&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;err&lt;/span&gt;&lt;span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;!=&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;/span&gt;&lt;span&gt;nil&lt;/span&gt;&lt;span&gt; {&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;17&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;    &lt;/span&gt;&lt;span&gt;return&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;false&lt;/span&gt;&lt;span&gt;, &lt;/span&gt;&lt;span&gt;err&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;18&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;&lt;span&gt;  &lt;/span&gt;&lt;/span&gt;&lt;span&gt;}&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;19&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;  &lt;/span&gt;&lt;span&gt;if&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;used&lt;/span&gt;&lt;span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;&amp;gt;&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;/span&gt;&lt;span&gt;limit&lt;/span&gt;&lt;span&gt; {&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;20&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;    &lt;/span&gt;&lt;span&gt;// 超限：把本次数量退回去，不放行&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;21&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;    &lt;/span&gt;&lt;span&gt;rdb&lt;/span&gt;&lt;span&gt;.&lt;/span&gt;&lt;span&gt;DecrBy&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;ctx&lt;/span&gt;&lt;span&gt;, &lt;/span&gt;&lt;span&gt;key&lt;/span&gt;&lt;span&gt;, &lt;/span&gt;&lt;span&gt;n&lt;/span&gt;&lt;span&gt;)&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;22&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;    &lt;/span&gt;&lt;span&gt;return&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;false&lt;/span&gt;&lt;span&gt;, &lt;/span&gt;&lt;span&gt;nil&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;23&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;&lt;span&gt;  &lt;/span&gt;&lt;/span&gt;&lt;span&gt;}&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;24&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;  &lt;/span&gt;&lt;span&gt;return&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;true&lt;/span&gt;&lt;span&gt;, &lt;/span&gt;&lt;span&gt;nil&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;25&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;}&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;/code&gt;&lt;/pre&gt;&lt;div&gt;&lt;div&gt;&lt;/div&gt;&lt;div&gt;&lt;/div&gt;&lt;/div&gt;&lt;/figure&gt;&lt;div&gt;&lt;/div&gt;&lt;/div&gt;&lt;span&gt;展开&lt;/span&gt;&lt;span&gt;收起&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;p&gt;这里有两个细节：&lt;/p&gt;&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;先 &lt;code&gt;SET NX&lt;/code&gt; 带过期时间，再 &lt;code&gt;INCRBY&lt;/code&gt;&lt;/strong&gt;。如果反过来先 &lt;code&gt;INCRBY&lt;/code&gt; 再单独 &lt;code&gt;EXPIRE&lt;/code&gt;，进程恰好在两步之间挂掉，这个键就永远不过期，配额会一直被占着。&lt;code&gt;SET NX&lt;/code&gt; 一条命令同时完成「创建」和「设置过期」，就没有这个问题。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;超限时的回退是安全的&lt;/strong&gt;。在 &lt;code&gt;INCRBY&lt;/code&gt; 和 &lt;code&gt;DECRBY&lt;/code&gt; 之间，其他服务可能短暂看到一个偏高的已用量，结果只是它们这一次也被拒绝、稍后重试，不会导致超发。宁可偶尔少放行，也不能多放行。&lt;/li&gt;
&lt;/ul&gt;&lt;p&gt;如果逻辑更复杂，比如要同时扣减「每秒频率」和「每日总量」两个计数器，并且要求两者同时成功或同时失败，就可以把这些步骤写成一段 Lua 脚本，在 Go 里用 &lt;code&gt;EVAL&lt;/code&gt; 调用，由 Redis 一次性执行完。单个计数器的场景，直接用命令就足够了。&lt;/p&gt;&lt;p&gt;拿不到配额的子任务不会被丢弃，而是重新投回队列，稍后再提交，和调用失败一样走 3 次重试的逻辑。&lt;/p&gt;&lt;p&gt;&lt;strong&gt;Redis 出问题怎么办？&lt;/strong&gt; 这里的选择是&lt;strong&gt;宁可停，也不超发&lt;/strong&gt;：申请配额时 Redis 报错，和配额不足一样处理，子任务重新入队重试，重试用完仍失败就标记为失败，绝不会在没有配额的情况下调用厂商接口。这是因为刷新、预热本身是异步任务，晚几分钟执行可以接受；而超出厂商限额，可能导致厂商侧报错甚至限制账号。另外，Redis 使用的是公司的高可用部署，实际很少出现不可用；每个主任务也有定时检查兜底，不会有任务卡住无人处理。&lt;/p&gt;&lt;p&gt;这里只是配额控制的基本思路，实际实现中还有不少细节没有展开，比如窗口边界上的计数、各厂商限额口径的差异（按 URL 数、按请求次数还是按目录）。&lt;/p&gt;&lt;/section&gt;
&lt;section&gt;&lt;h2&gt;七、失败重试与进度汇总&lt;a href=&quot;#七失败重试与进度汇总&quot;&gt;&lt;span&gt;#&lt;/span&gt;&lt;/a&gt;&lt;/h2&gt;&lt;p&gt;&lt;strong&gt;重试&lt;/strong&gt;：调用厂商接口失败（超时、限流、服务端错误）时，最多重试 3 次。仍然失败就不再重试，这个子任务标记为失败，但&lt;strong&gt;主任务照常完成&lt;/strong&gt;，只是完成结果里会体现哪些子任务、哪家厂商失败了。&lt;/p&gt;&lt;p&gt;这样做的考虑是：一家厂商出问题，不应该拖住其他厂商的结果。客户能清楚地看到「厂商 A、B 已刷新成功，厂商 C 失败」，再决定要不要单独重试。&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;th&gt;重试与检查策略&lt;/th&gt;&lt;/tr&gt;&lt;/thead&gt;&lt;tbody&gt;&lt;tr&gt;&lt;td&gt;刷新（URL、目录）&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;/tbody&gt;&lt;/table&gt;&lt;p&gt;比如某家厂商当天的刷新配额用完了，刷新任务会在几次重试后直接失败，客户能立刻看到，去厂商那里申请提额，而不是让任务悬而未决；预热任务则可以多等一会儿再试。&lt;/p&gt;&lt;p&gt;&lt;strong&gt;进度汇总&lt;/strong&gt;：厂商的刷新、预热都是异步的，提交成功之后还要等它真正执行完。所以每个主任务都会开启一个&lt;strong&gt;定时轮询&lt;/strong&gt;：&lt;/p&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;&lt;/span&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;&lt;/span&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;&lt;/span&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;&lt;/span&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;&lt;/span&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;&lt;/span&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;&lt;/span&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;&lt;/span&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;&lt;p&gt;主任务：刷新 3000 个 URL&lt;br /&gt;&lt;br /&gt;完成率 = 已结束子任务 / 子任务总数&lt;/p&gt;&lt;/span&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;&lt;p&gt;厂商 A（单次上限 1000）&lt;/p&gt;&lt;/span&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;&lt;p&gt;厂商 B（单次上限 500）&lt;/p&gt;&lt;/span&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;&lt;p&gt;子任务 A-1 ✅ 成功&lt;/p&gt;&lt;/span&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;&lt;p&gt;子任务 A-2 ✅ 成功&lt;/p&gt;&lt;/span&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;&lt;p&gt;子任务 A-3 ⏳ 执行中&lt;/p&gt;&lt;/span&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;&lt;p&gt;子任务 B-1 ✅ 成功&lt;/p&gt;&lt;/span&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;&lt;p&gt;子任务 B-2 ❌ 重试 3 次后失败&lt;/p&gt;&lt;/span&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;&lt;p&gt;……共 6 个&lt;/p&gt;&lt;/span&gt;&lt;/div&gt;
&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;&lt;/span&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;&lt;/span&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;&lt;/span&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;&lt;/span&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;&lt;/span&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;&lt;/span&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;&lt;/span&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;&lt;/span&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;&lt;p&gt;主任务：刷新 3000 个 URL&lt;br /&gt;&lt;br /&gt;完成率 = 已结束子任务 / 子任务总数&lt;/p&gt;&lt;/span&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;&lt;p&gt;厂商 A（单次上限 1000）&lt;/p&gt;&lt;/span&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;&lt;p&gt;厂商 B（单次上限 500）&lt;/p&gt;&lt;/span&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;&lt;p&gt;子任务 A-1 ✅ 成功&lt;/p&gt;&lt;/span&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;&lt;p&gt;子任务 A-2 ✅ 成功&lt;/p&gt;&lt;/span&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;&lt;p&gt;子任务 A-3 ⏳ 执行中&lt;/p&gt;&lt;/span&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;&lt;p&gt;子任务 B-1 ✅ 成功&lt;/p&gt;&lt;/span&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;&lt;p&gt;子任务 B-2 ❌ 重试 3 次后失败&lt;/p&gt;&lt;/span&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;&lt;p&gt;……共 6 个&lt;/p&gt;&lt;/span&gt;&lt;/div&gt;
&lt;/div&gt;&lt;/div&gt;&lt;/div&gt;&lt;p&gt;图中的厂商上限只是示意。同一批 URL 要分别提交给客户接入的每一家厂商，所以每家厂商各有一组子任务。&lt;/p&gt;&lt;ul&gt;
&lt;li&gt;查询所有未结束子任务在厂商侧的状态，经适配器翻译成统一状态后更新；&lt;/li&gt;
&lt;li&gt;根据子任务的状态，汇总出主任务的状态和&lt;strong&gt;完成率&lt;/strong&gt;；&lt;/li&gt;
&lt;li&gt;所有子任务都结束（成功或失败）后，主任务结束，停止轮询。&lt;/li&gt;
&lt;/ul&gt;&lt;p&gt;客户在多云的任务列表里看到的是一个主任务，点开可以看到它被拆成了哪些厂商的哪些子任务，以及每个子任务的执行情况。&lt;/p&gt;&lt;/section&gt;
&lt;section&gt;&lt;h2&gt;八、接入新厂商：从手工适配到 AI 辅助&lt;a href=&quot;#八接入新厂商从手工适配到-ai-辅助&quot;&gt;&lt;span&gt;#&lt;/span&gt;&lt;/a&gt;&lt;/h2&gt;&lt;p&gt;最初接入一家新厂商很慢。要先读完厂商的文档，弄清楚它的认证鉴权方式、内容管理的接口和限制，再手动写签名算法、请求和响应的结构体、状态映射，最后按适配器模式嵌入平台、联调测试。&lt;/p&gt;&lt;p&gt;后来我们开始借助 AI 来写适配器里最繁琐的部分：&lt;strong&gt;各厂商的认证鉴权实现，以及接口入参、出参的代码&lt;/strong&gt;。这部分本质上是把厂商文档翻译成代码，规则明确、重复性高，正好是 AI 擅长的。人主要负责的，变成了确认厂商的限制和边界情况、设计拆分策略、联调验证。&lt;/p&gt;&lt;p&gt;结果是，接入一家新厂商从原来的较长周期，缩短到&lt;strong&gt;一两天就能完成开发并发布上线&lt;/strong&gt;。&lt;/p&gt;&lt;p&gt;之所以能做到这一点，前提是前面的适配器设计：接口和限制的抽象已经稳定，新厂商只需要填空，AI 生成的代码有明确的边界和约束，验证起来也简单。&lt;/p&gt;&lt;/section&gt;
&lt;section&gt;&lt;h2&gt;九、总结&lt;a href=&quot;#九总结&quot;&gt;&lt;span&gt;#&lt;/span&gt;&lt;/a&gt;&lt;/h2&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;li&gt;&lt;strong&gt;Redis 共享配额&lt;/strong&gt;，用过期时间对齐厂商的限额窗口，用 INCRBY 的原子性保证多台服务不超发；&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;失败隔离&lt;/strong&gt;，一家厂商失败不影响其他厂商，结果透明地展示给客户；&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;稳定的抽象让 AI 辅助真正落地&lt;/strong&gt;，接入新厂商从「写一个系统」变成「填一个模板」。&lt;/li&gt;
&lt;/ol&gt;&lt;/section&gt;</content:encoded></item><item><title>从业务里长出来的状态机：一个资源建设审批流平台的设计与反思</title><link>https://tracerzzz.cn/posts/workflow-state-machine-design/</link><guid isPermaLink="true">https://tracerzzz.cn/posts/workflow-state-machine-design/</guid><description>我在字节从零设计开发过一个 CDN 资源建设流程平台，管理物理机、带宽、IP 从需求发起到交付的全过程，状态机和审批流都是自己实现的。这篇记录当时的设计：多级评审、乐观锁、基于 MongoDB 的变更 diff 审计、父子任务自动结单、事务化回滚、按平台协商的轮询；以及现在回头看，状态机和业务规则应该怎么解耦。</description><pubDate>Sun, 25 Oct 2026 09:00:00 GMT</pubDate><content:encoded>&lt;p&gt;在字节做 CDN 的时候，我从零设计开发过一个资源建设流程平台（内部叫 CRM）。它管的是 CDN 最主要的几项成本：物理机、带宽、IP。一个资源需求从发起、评审、资源团队评估，到派给各个建设平台、最终交付，整个过程都在这个平台上流转。后端用 Go 实现，由我一个人负责，状态机和审批流都是自己写的。&lt;/p&gt;
&lt;p&gt;这篇前半部分记录当时的设计，后半部分是回头看的反思：哪些地方做对了，哪些地方如果重来会换一种做法。文中的代码是按场景整理的示例，不是当时的原始实现。&lt;/p&gt;
&lt;section&gt;&lt;h2&gt;一、业务背景&lt;a href=&quot;#一业务背景&quot;&gt;&lt;span&gt;#&lt;/span&gt;&lt;/a&gt;&lt;/h2&gt;&lt;p&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;进度不透明&lt;/strong&gt;：一个需求卡在哪个环节、谁在处理，只能挨个问；&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;无法追溯&lt;/strong&gt;：事后想查某批机器是谁批的、数量被谁改过、什么时候交付的，找不到记录。&lt;/li&gt;
&lt;/ul&gt;&lt;p&gt;平台要做的事很明确：&lt;strong&gt;把流程线上化，每个节点的进度可视化、每一次变更都可追溯&lt;/strong&gt;。&lt;/p&gt;&lt;/section&gt;
&lt;section&gt;&lt;h2&gt;二、流程与状态&lt;a href=&quot;#二流程与状态&quot;&gt;&lt;span&gt;#&lt;/span&gt;&lt;/a&gt;&lt;/h2&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;&lt;/span&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;&lt;p&gt;提交&lt;/p&gt;&lt;/span&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;&lt;p&gt;驳回（记录原因）&lt;/p&gt;&lt;/span&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;&lt;p&gt;评审通过&lt;/p&gt;&lt;/span&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;&lt;p&gt;驳回（记录原因）&lt;/p&gt;&lt;/span&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;&lt;p&gt;评估通过，下发到各资源建设平台&lt;/p&gt;&lt;/span&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;&lt;p&gt;修改后重新提交&lt;/p&gt;&lt;/span&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;&lt;p&gt;所有子任务完成，自动结单&lt;/p&gt;&lt;/span&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;&lt;/span&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;&lt;p&gt;草稿&lt;/p&gt;&lt;/span&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;&lt;p&gt;待评审&lt;/p&gt;&lt;/span&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;&lt;p&gt;已驳回&lt;/p&gt;&lt;/span&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;&lt;p&gt;资源评估&lt;/p&gt;&lt;/span&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;&lt;p&gt;建设中&lt;/p&gt;&lt;/span&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;&lt;p&gt;已交付&lt;/p&gt;&lt;/span&gt;&lt;/div&gt;
&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;&lt;/span&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;&lt;p&gt;提交&lt;/p&gt;&lt;/span&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;&lt;p&gt;驳回（记录原因）&lt;/p&gt;&lt;/span&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;&lt;p&gt;评审通过&lt;/p&gt;&lt;/span&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;&lt;p&gt;驳回（记录原因）&lt;/p&gt;&lt;/span&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;&lt;p&gt;评估通过，下发到各资源建设平台&lt;/p&gt;&lt;/span&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;&lt;p&gt;修改后重新提交&lt;/p&gt;&lt;/span&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;&lt;p&gt;所有子任务完成，自动结单&lt;/p&gt;&lt;/span&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;&lt;/span&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;&lt;p&gt;草稿&lt;/p&gt;&lt;/span&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;&lt;p&gt;待评审&lt;/p&gt;&lt;/span&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;&lt;p&gt;已驳回&lt;/p&gt;&lt;/span&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;&lt;p&gt;资源评估&lt;/p&gt;&lt;/span&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;&lt;p&gt;建设中&lt;/p&gt;&lt;/span&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;&lt;p&gt;已交付&lt;/p&gt;&lt;/span&gt;&lt;/div&gt;
&lt;/div&gt;&lt;/div&gt;&lt;/div&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;/ol&gt;&lt;p&gt;两级都可以通过或驳回，&lt;strong&gt;每一次驳回都必须填写原因&lt;/strong&gt;并记录下来。被驳回的需求可以修改后重新提交，重新走一遍评审。&lt;/p&gt;&lt;p&gt;评估通过后，需求按资源类型拆成多个建设任务，下发到各类资源建设平台。每次状态变更，都会给相关的人发飞书消息：发起人、审批人、跟进人收到的链接不同，审批人点进去直接就是审批页面。&lt;/p&gt;&lt;p&gt;需求的入口是&lt;strong&gt;模板&lt;/strong&gt;：不同资源类型对应不同的模板，需求方必须按模板填写，精确到机器型号、带宽的服务区域和运营商等。需求在入口就是规范的，后面的评审和建设才有依据。&lt;/p&gt;&lt;/section&gt;
&lt;section&gt;&lt;h2&gt;三、几个关键设计&lt;a href=&quot;#三几个关键设计&quot;&gt;&lt;span&gt;#&lt;/span&gt;&lt;/a&gt;&lt;/h2&gt;&lt;section&gt;&lt;h3&gt;1. 并发控制：乐观锁&lt;a href=&quot;#1-并发控制乐观锁&quot;&gt;&lt;span&gt;#&lt;/span&gt;&lt;/a&gt;&lt;/h3&gt;&lt;p&gt;两个审批人同时操作同一个单子，比如一个点通过、一个点驳回，状态不能被改两次。用的是乐观锁：更新时把旧状态和版本号作为条件。&lt;/p&gt;&lt;div&gt;&lt;figure&gt;&lt;figcaption&gt;&lt;/figcaption&gt;&lt;pre&gt;&lt;code&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;1&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;UPDATE&lt;/span&gt;&lt;span&gt; requests&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;2&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;SET&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;state&lt;/span&gt;&lt;span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;=&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;/span&gt;&lt;span&gt;&apos;evaluating&apos;&lt;/span&gt;&lt;span&gt;, &lt;/span&gt;&lt;span&gt;version&lt;/span&gt;&lt;span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;=&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;/span&gt;&lt;span&gt;version&lt;/span&gt;&lt;span&gt; + &lt;/span&gt;&lt;span&gt;1&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;3&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;WHERE&lt;/span&gt;&lt;span&gt;&lt;span&gt; id &lt;/span&gt;&lt;span&gt;=&lt;/span&gt;&lt;span&gt; ? &lt;/span&gt;&lt;/span&gt;&lt;span&gt;AND&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;state&lt;/span&gt;&lt;span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;=&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;/span&gt;&lt;span&gt;&apos;reviewing&apos;&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;AND&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;version&lt;/span&gt;&lt;span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;=&lt;/span&gt;&lt;span&gt; ?;&lt;/span&gt;&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;/code&gt;&lt;/pre&gt;&lt;div&gt;&lt;div&gt;&lt;/div&gt;&lt;div&gt;&lt;/div&gt;&lt;/div&gt;&lt;/figure&gt;&lt;/div&gt;&lt;p&gt;影响行数为 0，说明单子已经被别人处理过了，这次操作直接失败，提示刷新。审批场景冲突很少，乐观锁比加锁更轻，也不会出现锁等待。&lt;/p&gt;&lt;/section&gt;&lt;section&gt;&lt;h3&gt;2. 变更审计：通用 diff，而不只是流转日志&lt;a href=&quot;#2-变更审计通用-diff而不只是流转日志&quot;&gt;&lt;span&gt;#&lt;/span&gt;&lt;/a&gt;&lt;/h3&gt;&lt;p&gt;「可追溯」是这个平台的核心诉求之一，我的做法比单纯记状态流转更进一步：&lt;/p&gt;&lt;ul&gt;
&lt;li&gt;单子本身落在业务库里；&lt;/li&gt;
&lt;li&gt;另有一个独立的日志库，用 &lt;strong&gt;MongoDB&lt;/strong&gt; 存储，因为日志是类 JSON 的半结构化数据，不同模板的字段各不相同，用文档库最自然；&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;每次状态变更，或者模板里任何字段的变更&lt;/strong&gt;（比如机器数量、资源类型），都会触发后端的 diff 逻辑：比较变更前后的单子，把差异记录进 diff 库。&lt;/li&gt;
&lt;/ul&gt;&lt;p&gt;所以在平台上，一个单子从创建到交付，每个字段被谁、在什么时候、从什么改成了什么，都能看到。&lt;/p&gt;&lt;p&gt;diff 的思路大致是这样（示例）：&lt;/p&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;&lt;figure&gt;&lt;figcaption&gt;&lt;/figcaption&gt;&lt;pre&gt;&lt;code&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;1&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;type&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;Change&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;struct&lt;/span&gt;&lt;span&gt; {&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;2&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;  &lt;/span&gt;&lt;span&gt;Field&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;string&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;`bson:&quot;field&quot;`&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;3&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;  &lt;/span&gt;&lt;span&gt;From&lt;/span&gt;&lt;span&gt;  &lt;/span&gt;&lt;span&gt;any&lt;/span&gt;&lt;span&gt;    &lt;/span&gt;&lt;span&gt;`bson:&quot;from&quot;`&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;4&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;  &lt;/span&gt;&lt;span&gt;To&lt;/span&gt;&lt;span&gt;    &lt;/span&gt;&lt;span&gt;any&lt;/span&gt;&lt;span&gt;    &lt;/span&gt;&lt;span&gt;`bson:&quot;to&quot;`&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;5&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;}&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;6&lt;/div&gt;&lt;/div&gt;&lt;div&gt;
&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;7&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;type&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;DiffLog&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;struct&lt;/span&gt;&lt;span&gt; {&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;8&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;  &lt;/span&gt;&lt;span&gt;RequestID&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;int64&lt;/span&gt;&lt;span&gt;     &lt;/span&gt;&lt;span&gt;`bson:&quot;request_id&quot;`&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;9&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;  &lt;/span&gt;&lt;span&gt;Operator&lt;/span&gt;&lt;span&gt;  &lt;/span&gt;&lt;span&gt;string&lt;/span&gt;&lt;span&gt;    &lt;/span&gt;&lt;span&gt;`bson:&quot;operator&quot;`&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;10&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;  &lt;/span&gt;&lt;span&gt;Kind&lt;/span&gt;&lt;span&gt;      &lt;/span&gt;&lt;span&gt;string&lt;/span&gt;&lt;span&gt;    &lt;/span&gt;&lt;span&gt;`bson:&quot;kind&quot;`&lt;/span&gt;&lt;span&gt;    &lt;/span&gt;&lt;span&gt;// &quot;state&quot; 状态变更 / &quot;field&quot; 字段变更&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;11&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;  &lt;/span&gt;&lt;span&gt;Changes&lt;/span&gt;&lt;span&gt;   []&lt;/span&gt;&lt;span&gt;Change&lt;/span&gt;&lt;span&gt;  &lt;/span&gt;&lt;span&gt;`bson:&quot;changes&quot;`&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;12&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;  &lt;/span&gt;&lt;span&gt;Comment&lt;/span&gt;&lt;span&gt;   &lt;/span&gt;&lt;span&gt;string&lt;/span&gt;&lt;span&gt;    &lt;/span&gt;&lt;span&gt;`bson:&quot;comment&quot;`&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;// 驳回原因等&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;13&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;  &lt;/span&gt;&lt;span&gt;CreatedAt&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;time&lt;/span&gt;&lt;span&gt;.&lt;/span&gt;&lt;span&gt;Time&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;`bson:&quot;created_at&quot;`&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;14&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;}&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;15&lt;/div&gt;&lt;/div&gt;&lt;div&gt;
&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;16&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;// 把单子转成 map 后逐个字段比较，嵌套的模板字段用点号路径表示，比如 &quot;spec.machine_count&quot;&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;17&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;func&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;diff&lt;/span&gt;&lt;span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;before&lt;/span&gt;&lt;span&gt;, &lt;/span&gt;&lt;span&gt;after&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;/span&gt;&lt;span&gt;map&lt;/span&gt;&lt;span&gt;[&lt;/span&gt;&lt;span&gt;string&lt;/span&gt;&lt;span&gt;]&lt;/span&gt;&lt;span&gt;any&lt;/span&gt;&lt;span&gt;&lt;span&gt;, &lt;/span&gt;&lt;span&gt;prefix&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;/span&gt;&lt;span&gt;string&lt;/span&gt;&lt;span&gt;) []&lt;/span&gt;&lt;span&gt;Change&lt;/span&gt;&lt;span&gt; {&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;18&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;  &lt;/span&gt;&lt;span&gt;var&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;out&lt;/span&gt;&lt;span&gt; []&lt;/span&gt;&lt;span&gt;Change&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;19&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;  &lt;/span&gt;&lt;span&gt;for&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;k&lt;/span&gt;&lt;span&gt;, &lt;/span&gt;&lt;span&gt;a&lt;/span&gt;&lt;span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;:=&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;/span&gt;&lt;span&gt;range&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;after&lt;/span&gt;&lt;span&gt; {&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;20&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;    &lt;/span&gt;&lt;span&gt;key&lt;/span&gt;&lt;span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;:=&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;/span&gt;&lt;span&gt;prefix&lt;/span&gt;&lt;span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;+&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;/span&gt;&lt;span&gt;k&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;21&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;    &lt;/span&gt;&lt;span&gt;b&lt;/span&gt;&lt;span&gt;, &lt;/span&gt;&lt;span&gt;ok&lt;/span&gt;&lt;span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;:=&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;/span&gt;&lt;span&gt;before&lt;/span&gt;&lt;span&gt;[&lt;/span&gt;&lt;span&gt;k&lt;/span&gt;&lt;span&gt;]&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;22&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;    &lt;/span&gt;&lt;span&gt;bm&lt;/span&gt;&lt;span&gt;, &lt;/span&gt;&lt;span&gt;bIsMap&lt;/span&gt;&lt;span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;:=&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;/span&gt;&lt;span&gt;b&lt;/span&gt;&lt;span&gt;.(&lt;/span&gt;&lt;span&gt;map&lt;/span&gt;&lt;span&gt;[&lt;/span&gt;&lt;span&gt;string&lt;/span&gt;&lt;span&gt;]&lt;/span&gt;&lt;span&gt;any&lt;/span&gt;&lt;span&gt;)&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;23&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;    &lt;/span&gt;&lt;span&gt;am&lt;/span&gt;&lt;span&gt;, &lt;/span&gt;&lt;span&gt;aIsMap&lt;/span&gt;&lt;span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;:=&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;/span&gt;&lt;span&gt;a&lt;/span&gt;&lt;span&gt;.(&lt;/span&gt;&lt;span&gt;map&lt;/span&gt;&lt;span&gt;[&lt;/span&gt;&lt;span&gt;string&lt;/span&gt;&lt;span&gt;]&lt;/span&gt;&lt;span&gt;any&lt;/span&gt;&lt;span&gt;)&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;24&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;    &lt;/span&gt;&lt;span&gt;switch&lt;/span&gt;&lt;span&gt; {&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;25&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;    &lt;/span&gt;&lt;span&gt;case&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;ok&lt;/span&gt;&lt;span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;&amp;amp;&amp;amp;&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;/span&gt;&lt;span&gt;bIsMap&lt;/span&gt;&lt;span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;&amp;amp;&amp;amp;&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;/span&gt;&lt;span&gt;aIsMap&lt;/span&gt;&lt;span&gt;:&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;26&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;      &lt;/span&gt;&lt;span&gt;out&lt;/span&gt;&lt;span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;=&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;/span&gt;&lt;span&gt;append&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;out&lt;/span&gt;&lt;span&gt;, &lt;/span&gt;&lt;span&gt;diff&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;bm&lt;/span&gt;&lt;span&gt;, &lt;/span&gt;&lt;span&gt;am&lt;/span&gt;&lt;span&gt;, &lt;/span&gt;&lt;span&gt;key&lt;/span&gt;&lt;span&gt;+&lt;/span&gt;&lt;span&gt;&quot;.&quot;&lt;/span&gt;&lt;span&gt;)...)&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;27&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;    &lt;/span&gt;&lt;span&gt;case&lt;/span&gt;&lt;span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;!&lt;/span&gt;&lt;/span&gt;&lt;span&gt;ok&lt;/span&gt;&lt;span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;||&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;!&lt;/span&gt;&lt;/span&gt;&lt;span&gt;reflect&lt;/span&gt;&lt;span&gt;.&lt;/span&gt;&lt;span&gt;DeepEqual&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;b&lt;/span&gt;&lt;span&gt;, &lt;/span&gt;&lt;span&gt;a&lt;/span&gt;&lt;span&gt;):&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;28&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;      &lt;/span&gt;&lt;span&gt;out&lt;/span&gt;&lt;span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;=&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;/span&gt;&lt;span&gt;append&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;out&lt;/span&gt;&lt;span&gt;, &lt;/span&gt;&lt;span&gt;Change&lt;/span&gt;&lt;span&gt;{&lt;/span&gt;&lt;span&gt;Field&lt;/span&gt;&lt;span&gt;: &lt;/span&gt;&lt;span&gt;key&lt;/span&gt;&lt;span&gt;, &lt;/span&gt;&lt;span&gt;From&lt;/span&gt;&lt;span&gt;: &lt;/span&gt;&lt;span&gt;b&lt;/span&gt;&lt;span&gt;, &lt;/span&gt;&lt;span&gt;To&lt;/span&gt;&lt;span&gt;: &lt;/span&gt;&lt;span&gt;a&lt;/span&gt;&lt;span&gt;})&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;29&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;&lt;span&gt;    &lt;/span&gt;&lt;/span&gt;&lt;span&gt;}&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;30&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;&lt;span&gt;  &lt;/span&gt;&lt;/span&gt;&lt;span&gt;}&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;31&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;  &lt;/span&gt;&lt;span&gt;for&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;k&lt;/span&gt;&lt;span&gt;, &lt;/span&gt;&lt;span&gt;b&lt;/span&gt;&lt;span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;:=&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;/span&gt;&lt;span&gt;range&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;before&lt;/span&gt;&lt;span&gt; {&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;32&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;    &lt;/span&gt;&lt;span&gt;if&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;_&lt;/span&gt;&lt;span&gt;, &lt;/span&gt;&lt;span&gt;ok&lt;/span&gt;&lt;span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;:=&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;/span&gt;&lt;span&gt;after&lt;/span&gt;&lt;span&gt;[&lt;/span&gt;&lt;span&gt;k&lt;/span&gt;&lt;span&gt;&lt;span&gt;]; &lt;/span&gt;&lt;span&gt;!&lt;/span&gt;&lt;/span&gt;&lt;span&gt;ok&lt;/span&gt;&lt;span&gt; {&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;33&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;      &lt;/span&gt;&lt;span&gt;out&lt;/span&gt;&lt;span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;=&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;/span&gt;&lt;span&gt;append&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;out&lt;/span&gt;&lt;span&gt;, &lt;/span&gt;&lt;span&gt;Change&lt;/span&gt;&lt;span&gt;{&lt;/span&gt;&lt;span&gt;Field&lt;/span&gt;&lt;span&gt;: &lt;/span&gt;&lt;span&gt;prefix&lt;/span&gt;&lt;span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;+&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;/span&gt;&lt;span&gt;k&lt;/span&gt;&lt;span&gt;, &lt;/span&gt;&lt;span&gt;From&lt;/span&gt;&lt;span&gt;: &lt;/span&gt;&lt;span&gt;b&lt;/span&gt;&lt;span&gt;, &lt;/span&gt;&lt;span&gt;To&lt;/span&gt;&lt;span&gt;: &lt;/span&gt;&lt;span&gt;nil&lt;/span&gt;&lt;span&gt;})&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;34&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;&lt;span&gt;    &lt;/span&gt;&lt;/span&gt;&lt;span&gt;}&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;35&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;&lt;span&gt;  &lt;/span&gt;&lt;/span&gt;&lt;span&gt;}&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;36&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;  &lt;/span&gt;&lt;span&gt;return&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;out&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;37&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;}&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;/code&gt;&lt;/pre&gt;&lt;div&gt;&lt;div&gt;&lt;/div&gt;&lt;div&gt;&lt;/div&gt;&lt;/div&gt;&lt;/figure&gt;&lt;div&gt;&lt;/div&gt;&lt;/div&gt;&lt;span&gt;展开&lt;/span&gt;&lt;span&gt;收起&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;p&gt;前端的流程图也是建立在这份数据上的：&lt;strong&gt;只查询 &lt;code&gt;kind = state&lt;/code&gt; 的记录，按时间排列，就是流转记录&lt;/strong&gt;；节点详情里的审批意见、驳回原因、交付明细，也都来自这里。一份数据，同时支撑了「流程图」和「完整变更历史」两个视图。&lt;/p&gt;&lt;/section&gt;&lt;section&gt;&lt;h3&gt;3. 父子任务与自动结单&lt;a href=&quot;#3-父子任务与自动结单&quot;&gt;&lt;span&gt;#&lt;/span&gt;&lt;/a&gt;&lt;/h3&gt;&lt;p&gt;一个需求往往同时包含机器、IP、带宽，由不同的建设平台负责，进度各不相同。所以评估通过后会拆成多个&lt;strong&gt;子任务&lt;/strong&gt;分别跟踪，父任务是需求单本身。&lt;/p&gt;&lt;p&gt;收尾不靠人工确认，而是&lt;strong&gt;定时任务&lt;/strong&gt;校验：所有子任务都已交付时，自动把父任务变更为已交付，完成结单。&lt;/p&gt;&lt;/section&gt;&lt;section&gt;&lt;h3&gt;4. 和各资源建设平台对接：状态对齐与轮询&lt;a href=&quot;#4-和各资源建设平台对接状态对齐与轮询&quot;&gt;&lt;span&gt;#&lt;/span&gt;&lt;/a&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;/p&gt;&lt;/section&gt;&lt;section&gt;&lt;h3&gt;5. 失败处理：操作事务化与回滚&lt;a href=&quot;#5-失败处理操作事务化与回滚&quot;&gt;&lt;span&gt;#&lt;/span&gt;&lt;/a&gt;&lt;/h3&gt;&lt;p&gt;一次操作可能包含多个步骤，比如评估通过后，要把多个子任务分别下发到不同的建设平台。如果其中一个失败了，单子就会处于「部分下发」的尴尬状态。&lt;/p&gt;&lt;p&gt;当时的做法是&lt;strong&gt;把一次操作事务化&lt;/strong&gt;：流程中任何一个子项失败，整个操作回滚，单子回到操作前的状态；失败和回滚都会发消息通知相关的人，处理完问题后重新操作即可。这样保证了单子的状态永远是一致的，不会出现一半成功、一半失败。&lt;/p&gt;&lt;/section&gt;&lt;/section&gt;
&lt;section&gt;&lt;h2&gt;四、回头看：做对了什么&lt;a href=&quot;#四回头看做对了什么&quot;&gt;&lt;span&gt;#&lt;/span&gt;&lt;/a&gt;&lt;/h2&gt;&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;先解决业务问题&lt;/strong&gt;。平台最大的价值不在技术，而是把一个靠人推动的流程，变成了规范、透明、可追溯的线上流程。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;审计做成了通用能力&lt;/strong&gt;。diff 机制不关心具体是哪个字段，新增模板字段不用改审计逻辑，就自动有了变更记录。&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;/ul&gt;&lt;/section&gt;
&lt;section&gt;&lt;h2&gt;五、回头看：如果重来会改什么&lt;a href=&quot;#五回头看如果重来会改什么&quot;&gt;&lt;span&gt;#&lt;/span&gt;&lt;/a&gt;&lt;/h2&gt;&lt;section&gt;&lt;h3&gt;1. 状态机和业务规则解耦&lt;a href=&quot;#1-状态机和业务规则解耦&quot;&gt;&lt;span&gt;#&lt;/span&gt;&lt;/a&gt;&lt;/h3&gt;&lt;p&gt;当时最大的问题是：状态流转是按业务需求一点点实现的。某个节点只能到某个节点、某些节点不能驳回、某类资源由哪些团队评估，这些规则散落在状态处理的代码里，和流转本身绑在一起。规则越加越多，改规则就要改代码、发版本，一个状态能去哪里也要把好几处代码拼起来才看得清。&lt;/p&gt;&lt;p&gt;现在会把「当前状态 + 事件 → 目标状态」抽成一张&lt;strong&gt;转移表&lt;/strong&gt;，状态机核心只负责查表，业务规则通过 Guard（流转前校验）和 Action（流转后动作）挂上去：&lt;/p&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;&lt;figure&gt;&lt;figcaption&gt;&lt;/figcaption&gt;&lt;pre&gt;&lt;code&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;1&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;type&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;Transition&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;struct&lt;/span&gt;&lt;span&gt; {&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;2&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;  &lt;/span&gt;&lt;span&gt;From&lt;/span&gt;&lt;span&gt;   &lt;/span&gt;&lt;span&gt;State&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;3&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;  &lt;/span&gt;&lt;span&gt;Event&lt;/span&gt;&lt;span&gt;  &lt;/span&gt;&lt;span&gt;Event&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;4&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;  &lt;/span&gt;&lt;span&gt;To&lt;/span&gt;&lt;span&gt;     &lt;/span&gt;&lt;span&gt;State&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;5&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;  &lt;/span&gt;&lt;span&gt;Guard&lt;/span&gt;&lt;span&gt;  &lt;/span&gt;&lt;span&gt;func&lt;/span&gt;&lt;span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;*&lt;/span&gt;&lt;/span&gt;&lt;span&gt;Ctx&lt;/span&gt;&lt;span&gt;) &lt;/span&gt;&lt;span&gt;error&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;// 返回 error 则拒绝，比如操作人不是该资源类型的评估人&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;6&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;  &lt;/span&gt;&lt;span&gt;Action&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;func&lt;/span&gt;&lt;span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;*&lt;/span&gt;&lt;/span&gt;&lt;span&gt;Ctx&lt;/span&gt;&lt;span&gt;) &lt;/span&gt;&lt;span&gt;error&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;// 流转后的动作，比如拆分并下发子任务&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;7&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;}&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;8&lt;/div&gt;&lt;/div&gt;&lt;div&gt;
&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;9&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;var&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;transitions&lt;/span&gt;&lt;span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;=&lt;/span&gt;&lt;span&gt; []&lt;/span&gt;&lt;/span&gt;&lt;span&gt;Transition&lt;/span&gt;&lt;span&gt;{&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;10&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;&lt;span&gt;  &lt;/span&gt;&lt;/span&gt;&lt;span&gt;{&lt;/span&gt;&lt;span&gt;From&lt;/span&gt;&lt;span&gt;: &lt;/span&gt;&lt;span&gt;Draft&lt;/span&gt;&lt;span&gt;, &lt;/span&gt;&lt;span&gt;Event&lt;/span&gt;&lt;span&gt;: &lt;/span&gt;&lt;span&gt;Submit&lt;/span&gt;&lt;span&gt;, &lt;/span&gt;&lt;span&gt;To&lt;/span&gt;&lt;span&gt;: &lt;/span&gt;&lt;span&gt;Reviewing&lt;/span&gt;&lt;span&gt;, &lt;/span&gt;&lt;span&gt;Guard&lt;/span&gt;&lt;span&gt;: &lt;/span&gt;&lt;span&gt;requireTemplateComplete&lt;/span&gt;&lt;span&gt;},&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;11&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;&lt;span&gt;  &lt;/span&gt;&lt;/span&gt;&lt;span&gt;{&lt;/span&gt;&lt;span&gt;From&lt;/span&gt;&lt;span&gt;: &lt;/span&gt;&lt;span&gt;Reviewing&lt;/span&gt;&lt;span&gt;, &lt;/span&gt;&lt;span&gt;Event&lt;/span&gt;&lt;span&gt;: &lt;/span&gt;&lt;span&gt;Approve&lt;/span&gt;&lt;span&gt;, &lt;/span&gt;&lt;span&gt;To&lt;/span&gt;&lt;span&gt;: &lt;/span&gt;&lt;span&gt;Evaluating&lt;/span&gt;&lt;span&gt;, &lt;/span&gt;&lt;span&gt;Guard&lt;/span&gt;&lt;span&gt;: &lt;/span&gt;&lt;span&gt;requireReviewer&lt;/span&gt;&lt;span&gt;},&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;12&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;&lt;span&gt;  &lt;/span&gt;&lt;/span&gt;&lt;span&gt;{&lt;/span&gt;&lt;span&gt;From&lt;/span&gt;&lt;span&gt;: &lt;/span&gt;&lt;span&gt;Reviewing&lt;/span&gt;&lt;span&gt;, &lt;/span&gt;&lt;span&gt;Event&lt;/span&gt;&lt;span&gt;: &lt;/span&gt;&lt;span&gt;Reject&lt;/span&gt;&lt;span&gt;, &lt;/span&gt;&lt;span&gt;To&lt;/span&gt;&lt;span&gt;: &lt;/span&gt;&lt;span&gt;Rejected&lt;/span&gt;&lt;span&gt;, &lt;/span&gt;&lt;span&gt;Guard&lt;/span&gt;&lt;span&gt;: &lt;/span&gt;&lt;span&gt;requireReviewerAndReason&lt;/span&gt;&lt;span&gt;},&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;13&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;&lt;span&gt;  &lt;/span&gt;&lt;/span&gt;&lt;span&gt;{&lt;/span&gt;&lt;span&gt;From&lt;/span&gt;&lt;span&gt;: &lt;/span&gt;&lt;span&gt;Evaluating&lt;/span&gt;&lt;span&gt;, &lt;/span&gt;&lt;span&gt;Event&lt;/span&gt;&lt;span&gt;: &lt;/span&gt;&lt;span&gt;Approve&lt;/span&gt;&lt;span&gt;, &lt;/span&gt;&lt;span&gt;To&lt;/span&gt;&lt;span&gt;: &lt;/span&gt;&lt;span&gt;Building&lt;/span&gt;&lt;span&gt;, &lt;/span&gt;&lt;span&gt;Guard&lt;/span&gt;&lt;span&gt;: &lt;/span&gt;&lt;span&gt;requireEvaluator&lt;/span&gt;&lt;span&gt;, &lt;/span&gt;&lt;span&gt;Action&lt;/span&gt;&lt;span&gt;: &lt;/span&gt;&lt;span&gt;dispatchSubTasks&lt;/span&gt;&lt;span&gt;},&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;14&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;&lt;span&gt;  &lt;/span&gt;&lt;/span&gt;&lt;span&gt;{&lt;/span&gt;&lt;span&gt;From&lt;/span&gt;&lt;span&gt;: &lt;/span&gt;&lt;span&gt;Evaluating&lt;/span&gt;&lt;span&gt;, &lt;/span&gt;&lt;span&gt;Event&lt;/span&gt;&lt;span&gt;: &lt;/span&gt;&lt;span&gt;Reject&lt;/span&gt;&lt;span&gt;, &lt;/span&gt;&lt;span&gt;To&lt;/span&gt;&lt;span&gt;: &lt;/span&gt;&lt;span&gt;Rejected&lt;/span&gt;&lt;span&gt;, &lt;/span&gt;&lt;span&gt;Guard&lt;/span&gt;&lt;span&gt;: &lt;/span&gt;&lt;span&gt;requireEvaluatorAndReason&lt;/span&gt;&lt;span&gt;},&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;15&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;&lt;span&gt;  &lt;/span&gt;&lt;/span&gt;&lt;span&gt;{&lt;/span&gt;&lt;span&gt;From&lt;/span&gt;&lt;span&gt;: &lt;/span&gt;&lt;span&gt;Rejected&lt;/span&gt;&lt;span&gt;, &lt;/span&gt;&lt;span&gt;Event&lt;/span&gt;&lt;span&gt;: &lt;/span&gt;&lt;span&gt;Submit&lt;/span&gt;&lt;span&gt;, &lt;/span&gt;&lt;span&gt;To&lt;/span&gt;&lt;span&gt;: &lt;/span&gt;&lt;span&gt;Reviewing&lt;/span&gt;&lt;span&gt;, &lt;/span&gt;&lt;span&gt;Guard&lt;/span&gt;&lt;span&gt;: &lt;/span&gt;&lt;span&gt;requireApplicant&lt;/span&gt;&lt;span&gt;},&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;16&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;&lt;span&gt;  &lt;/span&gt;&lt;/span&gt;&lt;span&gt;{&lt;/span&gt;&lt;span&gt;From&lt;/span&gt;&lt;span&gt;: &lt;/span&gt;&lt;span&gt;Building&lt;/span&gt;&lt;span&gt;, &lt;/span&gt;&lt;span&gt;Event&lt;/span&gt;&lt;span&gt;: &lt;/span&gt;&lt;span&gt;Deliver&lt;/span&gt;&lt;span&gt;, &lt;/span&gt;&lt;span&gt;To&lt;/span&gt;&lt;span&gt;: &lt;/span&gt;&lt;span&gt;Delivered&lt;/span&gt;&lt;span&gt;, &lt;/span&gt;&lt;span&gt;Guard&lt;/span&gt;&lt;span&gt;: &lt;/span&gt;&lt;span&gt;requireAllSubTasksDone&lt;/span&gt;&lt;span&gt;},&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;17&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;}&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;/code&gt;&lt;/pre&gt;&lt;div&gt;&lt;div&gt;&lt;/div&gt;&lt;div&gt;&lt;/div&gt;&lt;/div&gt;&lt;/figure&gt;&lt;div&gt;&lt;/div&gt;&lt;/div&gt;&lt;span&gt;展开&lt;/span&gt;&lt;span&gt;收起&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;p&gt;「某个节点只能到某些节点」就是表里有没有这一行，「某个节点不能驳回」就是这个状态没有 &lt;code&gt;Reject&lt;/code&gt; 这一行。变化最频繁的「哪类资源由哪些团队评估」，则放到配置表里由 Guard 读取，调整团队不用发版本：&lt;/p&gt;&lt;div&gt;&lt;figure&gt;&lt;figcaption&gt;&lt;/figcaption&gt;&lt;pre&gt;&lt;code&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;1&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;- &lt;/span&gt;&lt;span&gt;resource_type&lt;/span&gt;&lt;span&gt;: &lt;/span&gt;&lt;span&gt;bandwidth&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;2&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;  &lt;/span&gt;&lt;span&gt;evaluators&lt;/span&gt;&lt;span&gt;: [&lt;/span&gt;&lt;span&gt;network-team&lt;/span&gt;&lt;span&gt;, &lt;/span&gt;&lt;span&gt;cost-team&lt;/span&gt;&lt;span&gt;]&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;3&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;- &lt;/span&gt;&lt;span&gt;resource_type&lt;/span&gt;&lt;span&gt;: &lt;/span&gt;&lt;span&gt;physical_machine&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;4&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;  &lt;/span&gt;&lt;span&gt;evaluators&lt;/span&gt;&lt;span&gt;: [&lt;/span&gt;&lt;span&gt;server-team&lt;/span&gt;&lt;span&gt;, &lt;/span&gt;&lt;span&gt;cost-team&lt;/span&gt;&lt;span&gt;]&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;5&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;- &lt;/span&gt;&lt;span&gt;resource_type&lt;/span&gt;&lt;span&gt;: &lt;/span&gt;&lt;span&gt;ip&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;6&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;  &lt;/span&gt;&lt;span&gt;evaluators&lt;/span&gt;&lt;span&gt;: [&lt;/span&gt;&lt;span&gt;network-team&lt;/span&gt;&lt;span&gt;]&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;/code&gt;&lt;/pre&gt;&lt;div&gt;&lt;div&gt;&lt;/div&gt;&lt;div&gt;&lt;/div&gt;&lt;/div&gt;&lt;/figure&gt;&lt;/div&gt;&lt;/section&gt;&lt;section&gt;&lt;h3&gt;2. 区分两类副作用&lt;a href=&quot;#2-区分两类副作用&quot;&gt;&lt;span&gt;#&lt;/span&gt;&lt;/a&gt;&lt;/h3&gt;&lt;p&gt;当时把一次操作整体事务化、失败就回滚，保证了一致性。回头看，副作用其实分两类，可以分开处理：&lt;/p&gt;&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;决定业务结果的&lt;/strong&gt;，比如下发建设任务：失败了单子就不该推进，回滚是对的。但要注意，回滚数据库很容易，已经在外部平台创建成功的任务却回滚不了，需要调用对方的取消接口做&lt;strong&gt;补偿&lt;/strong&gt;，这就是 Saga 模式的思路。所以下发前最好先做预校验，下发接口本身也要幂等，避免重试时重复建单。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;不影响业务结果的&lt;/strong&gt;，比如发飞书通知：不应该因为通知发失败就回滚整个审批。更好的做法是&lt;strong&gt;事务性发件箱（Outbox）&lt;/strong&gt;：在同一个事务里把「待发送的通知」写进一张表，事务提交后由后台任务发送，失败重试。&lt;/li&gt;
&lt;/ul&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;&lt;/span&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;&lt;/span&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;&lt;/span&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;&lt;/span&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;&lt;p&gt;失败则重试&lt;/p&gt;&lt;/span&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;&lt;p&gt;审批操作&lt;/p&gt;&lt;/span&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;&lt;p&gt;事务：更新状态 + 写 outbox&lt;/p&gt;&lt;/span&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;&lt;p&gt;outbox 表&lt;/p&gt;&lt;/span&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;&lt;p&gt;后台任务&lt;/p&gt;&lt;/span&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;&lt;p&gt;发送飞书通知&lt;/p&gt;&lt;/span&gt;&lt;/div&gt;
&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;&lt;/span&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;&lt;/span&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;&lt;/span&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;&lt;/span&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;&lt;p&gt;失败则重试&lt;/p&gt;&lt;/span&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;&lt;p&gt;审批操作&lt;/p&gt;&lt;/span&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;&lt;p&gt;事务：更新状态 + 写 outbox&lt;/p&gt;&lt;/span&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;&lt;p&gt;outbox 表&lt;/p&gt;&lt;/span&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;&lt;p&gt;后台任务&lt;/p&gt;&lt;/span&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;&lt;p&gt;发送飞书通知&lt;/p&gt;&lt;/span&gt;&lt;/div&gt;
&lt;/div&gt;&lt;/div&gt;&lt;/div&gt;&lt;/section&gt;&lt;section&gt;&lt;h3&gt;3. 为什么没用现成的工作流引擎&lt;a href=&quot;#3-为什么没用现成的工作流引擎&quot;&gt;&lt;span&gt;#&lt;/span&gt;&lt;/a&gt;&lt;/h3&gt;&lt;p&gt;Java 生态有 Camunda、Flowable 这类 BPMN 引擎，Go 生态有 Temporal 这样的编排框架，也有 &lt;code&gt;looplab/fsm&lt;/code&gt;、&lt;code&gt;qmuntal/stateless&lt;/code&gt; 这类状态机库。在这个场景下，自己实现是合理的：&lt;/p&gt;&lt;ul&gt;
&lt;li&gt;流程本身不复杂，状态不多，引入完整的工作流引擎，运维和学习成本可能比收益还大；&lt;/li&gt;
&lt;li&gt;审批规则和公司内部的组织架构、资源平台深度绑定，引擎能省掉的只是很薄的状态流转那一层；&lt;/li&gt;
&lt;li&gt;后端只有我一个人负责，一个轻量、完全掌控的实现更容易维护。&lt;/li&gt;
&lt;/ul&gt;&lt;p&gt;但如果流程变得很复杂，比如有并行会签、超时自动升级、需要业务人员自己拖拽配置流程，现成的引擎就值得考虑了。选型的关键不是「自研好还是开源好」，而是&lt;strong&gt;复杂度在哪一层&lt;/strong&gt;。&lt;/p&gt;&lt;/section&gt;&lt;/section&gt;
&lt;section&gt;&lt;h2&gt;六、总结&lt;a href=&quot;#六总结&quot;&gt;&lt;span&gt;#&lt;/span&gt;&lt;/a&gt;&lt;/h2&gt;&lt;p&gt;这个平台的状态机，是从业务里一点点长出来的。它在一致性（乐观锁、事务化回滚）和可追溯（通用 diff 审计）上做得比较扎实，这两点也正是审批类系统最核心的要求。最大的遗憾是状态流转和业务规则耦合在一起。如果重来，第一件事就是把稳定的状态机核心和易变的业务规则分开：前者用转移表一次写好，后者交给配置。&lt;/p&gt;&lt;/section&gt;</content:encoded></item><item><title>指标异动归因：从贡献度公式到 CDN 毛利率的层层下钻</title><link>https://tracerzzz.cn/posts/metric-anomaly-attribution/</link><guid isPermaLink="true">https://tracerzzz.cn/posts/metric-anomaly-attribution/</guid><description>整理我学习指标异动归因的过程：加法、乘法、比率三类指标分别怎么算贡献度，为什么比率指标要拆出「结构变化」，以及我在字节火山 CDN 经营分析中，如何用这套方法对毛利率、收入、成本做逐层递归归因，把收入变化拆到客户增减、用量变化和价格变化。</description><pubDate>Sun, 25 Oct 2026 06:00:00 GMT</pubDate><content:encoded>&lt;p&gt;在字节做火山 CDN 经营分析时，看板每天都会出数，领导最常问的不是「毛利率是多少」，而是「&lt;strong&gt;毛利率为什么掉了？&lt;/strong&gt;」。一张看板只能告诉你结果，回答「为什么」得靠归因。&lt;/p&gt;
&lt;p&gt;这篇文章记录我当时学习归因分析的过程，以及最后落地到经营分析平台上的做法。方法主要参考了 CSDN 上一篇讲贡献度计算的文章（见文末参考），这里按我自己的理解重新梳理，例子都换成了 CDN 场景。文中数字均为示意，不是真实业务数据。&lt;/p&gt;
&lt;section&gt;&lt;h2&gt;一、核心思路：顺着公式往下拆&lt;a href=&quot;#一核心思路顺着公式往下拆&quot;&gt;&lt;span&gt;#&lt;/span&gt;&lt;/a&gt;&lt;/h2&gt;&lt;p&gt;归因的出发点很简单：&lt;strong&gt;每个指标都有计算公式，指标变了，一定是公式里的某个子指标变了。&lt;/strong&gt;&lt;/p&gt;&lt;p&gt;所以归因就是两步，反复递归：&lt;/p&gt;&lt;ol&gt;
&lt;li&gt;把指标按公式拆成子指标；&lt;/li&gt;
&lt;li&gt;算出每个子指标对这次变化的&lt;strong&gt;贡献度&lt;/strong&gt;，找出贡献最大的那个，再对它继续拆。&lt;/li&gt;
&lt;/ol&gt;&lt;p&gt;难点在第 2 步：公式的形态不同，贡献度的算法也不同。按公式形态，指标可以分成三类。&lt;/p&gt;&lt;/section&gt;
&lt;section&gt;&lt;h2&gt;二、加法型指标：直接看差值&lt;a href=&quot;#二加法型指标直接看差值&quot;&gt;&lt;span&gt;#&lt;/span&gt;&lt;/a&gt;&lt;/h2&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;span&gt;&lt;span&gt;&lt;span&gt;Ci=ΔxiΔY,Y=∑ixiC_i = \frac{\Delta x_i}{\Delta Y},\quad Y=\sum_i x_i&lt;/span&gt;&lt;span&gt;&lt;span&gt;&lt;span&gt;&lt;/span&gt;&lt;span&gt;&lt;span&gt;C&lt;/span&gt;&lt;span&gt;&lt;span&gt;&lt;span&gt;&lt;span&gt;&lt;span&gt;&lt;span&gt;&lt;/span&gt;&lt;span&gt;&lt;span&gt;i&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span&gt;​&lt;/span&gt;&lt;/span&gt;&lt;span&gt;&lt;span&gt;&lt;span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span&gt;&lt;/span&gt;&lt;span&gt;=&lt;/span&gt;&lt;span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span&gt;&lt;span&gt;&lt;/span&gt;&lt;span&gt;&lt;span&gt;&lt;/span&gt;&lt;span&gt;&lt;span&gt;&lt;span&gt;&lt;span&gt;&lt;span&gt;&lt;span&gt;&lt;/span&gt;&lt;span&gt;&lt;span&gt;Δ&lt;/span&gt;&lt;span&gt;Y&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span&gt;&lt;span&gt;&lt;/span&gt;&lt;span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span&gt;&lt;span&gt;&lt;/span&gt;&lt;span&gt;&lt;span&gt;Δ&lt;/span&gt;&lt;span&gt;&lt;span&gt;x&lt;/span&gt;&lt;span&gt;&lt;span&gt;&lt;span&gt;&lt;span&gt;&lt;span&gt;&lt;span&gt;&lt;/span&gt;&lt;span&gt;&lt;span&gt;i&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span&gt;​&lt;/span&gt;&lt;/span&gt;&lt;span&gt;&lt;span&gt;&lt;span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span&gt;​&lt;/span&gt;&lt;/span&gt;&lt;span&gt;&lt;span&gt;&lt;span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span&gt;,&lt;/span&gt;&lt;span&gt;&lt;/span&gt;&lt;span&gt;&lt;/span&gt;&lt;span&gt;Y&lt;/span&gt;&lt;span&gt;&lt;/span&gt;&lt;span&gt;=&lt;/span&gt;&lt;span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span&gt;&lt;span&gt;&lt;/span&gt;&lt;span&gt;&lt;span&gt;&lt;span&gt;&lt;span&gt;&lt;span&gt;&lt;span&gt;&lt;/span&gt;&lt;span&gt;&lt;span&gt;i&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span&gt;&lt;span&gt;&lt;/span&gt;&lt;span&gt;&lt;span&gt;∑&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span&gt;​&lt;/span&gt;&lt;/span&gt;&lt;span&gt;&lt;span&gt;&lt;span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span&gt;&lt;/span&gt;&lt;span&gt;&lt;span&gt;x&lt;/span&gt;&lt;span&gt;&lt;span&gt;&lt;span&gt;&lt;span&gt;&lt;span&gt;&lt;span&gt;&lt;/span&gt;&lt;span&gt;&lt;span&gt;i&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span&gt;​&lt;/span&gt;&lt;/span&gt;&lt;span&gt;&lt;span&gt;&lt;span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;p&gt;&lt;strong&gt;例子&lt;/strong&gt;：本月总成本比上月涨了 100 万，其中客户 A 涨了 80 万，客户 B 涨了 30 万，客户 C 降了 10 万。那么 A 的贡献度是 80%，B 是 30%，C 是 -10%，加起来正好 100%。&lt;/p&gt;&lt;p&gt;这是最直观的一类，注意贡献度可以是负数，也可以超过 100%（有正有负相互抵消时）。&lt;/p&gt;&lt;/section&gt;
&lt;section&gt;&lt;h2&gt;三、乘法型指标：用对数拆开&lt;a href=&quot;#三乘法型指标用对数拆开&quot;&gt;&lt;span&gt;#&lt;/span&gt;&lt;/a&gt;&lt;/h2&gt;&lt;p&gt;&lt;strong&gt;形态&lt;/strong&gt;：指标 = 几个因子相乘。比如带宽成本 = 计费带宽 × 单价。&lt;/p&gt;&lt;p&gt;乘法的麻烦在于因子之间会「交叉影响」：带宽涨了 20%、单价降了 10%，总的变化里有一部分是两者叠加出来的，没法简单归给某一方。&lt;/p&gt;&lt;p&gt;常用的解法是 &lt;strong&gt;LMDI（对数平均迪氏指数分解）&lt;/strong&gt;。思路是取对数，把乘法变成加法：&lt;/p&gt;&lt;span&gt;&lt;span&gt;&lt;span&gt;ln⁡Y1Y0=∑iln⁡xi1xi0\ln\frac{Y^1}{Y^0} = \sum_i \ln\frac{x_i^1}{x_i^0}&lt;/span&gt;&lt;span&gt;&lt;span&gt;&lt;span&gt;&lt;/span&gt;&lt;span&gt;ln&lt;/span&gt;&lt;span&gt;&lt;/span&gt;&lt;span&gt;&lt;span&gt;&lt;/span&gt;&lt;span&gt;&lt;span&gt;&lt;span&gt;&lt;span&gt;&lt;span&gt;&lt;span&gt;&lt;/span&gt;&lt;span&gt;&lt;span&gt;&lt;span&gt;Y&lt;/span&gt;&lt;span&gt;&lt;span&gt;&lt;span&gt;&lt;span&gt;&lt;span&gt;&lt;span&gt;&lt;/span&gt;&lt;span&gt;&lt;span&gt;0&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span&gt;&lt;span&gt;&lt;/span&gt;&lt;span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span&gt;&lt;span&gt;&lt;/span&gt;&lt;span&gt;&lt;span&gt;&lt;span&gt;Y&lt;/span&gt;&lt;span&gt;&lt;span&gt;&lt;span&gt;&lt;span&gt;&lt;span&gt;&lt;span&gt;&lt;/span&gt;&lt;span&gt;&lt;span&gt;1&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span&gt;​&lt;/span&gt;&lt;/span&gt;&lt;span&gt;&lt;span&gt;&lt;span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span&gt;&lt;/span&gt;&lt;span&gt;=&lt;/span&gt;&lt;span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span&gt;&lt;span&gt;&lt;/span&gt;&lt;span&gt;&lt;span&gt;&lt;span&gt;&lt;span&gt;&lt;span&gt;&lt;span&gt;&lt;/span&gt;&lt;span&gt;&lt;span&gt;i&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span&gt;&lt;span&gt;&lt;/span&gt;&lt;span&gt;&lt;span&gt;∑&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span&gt;​&lt;/span&gt;&lt;/span&gt;&lt;span&gt;&lt;span&gt;&lt;span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span&gt;&lt;/span&gt;&lt;span&gt;ln&lt;/span&gt;&lt;span&gt;&lt;/span&gt;&lt;span&gt;&lt;span&gt;&lt;/span&gt;&lt;span&gt;&lt;span&gt;&lt;span&gt;&lt;span&gt;&lt;span&gt;&lt;span&gt;&lt;/span&gt;&lt;span&gt;&lt;span&gt;&lt;span&gt;x&lt;/span&gt;&lt;span&gt;&lt;span&gt;&lt;span&gt;&lt;span&gt;&lt;span&gt;&lt;span&gt;&lt;/span&gt;&lt;span&gt;&lt;span&gt;i&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span&gt;&lt;span&gt;&lt;/span&gt;&lt;span&gt;&lt;span&gt;0&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span&gt;​&lt;/span&gt;&lt;/span&gt;&lt;span&gt;&lt;span&gt;&lt;span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span&gt;&lt;span&gt;&lt;/span&gt;&lt;span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span&gt;&lt;span&gt;&lt;/span&gt;&lt;span&gt;&lt;span&gt;&lt;span&gt;x&lt;/span&gt;&lt;span&gt;&lt;span&gt;&lt;span&gt;&lt;span&gt;&lt;span&gt;&lt;span&gt;&lt;/span&gt;&lt;span&gt;&lt;span&gt;i&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span&gt;&lt;span&gt;&lt;/span&gt;&lt;span&gt;&lt;span&gt;1&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span&gt;​&lt;/span&gt;&lt;/span&gt;&lt;span&gt;&lt;span&gt;&lt;span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span&gt;​&lt;/span&gt;&lt;/span&gt;&lt;span&gt;&lt;span&gt;&lt;span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;p&gt;再乘上一个对数平均权重，把每个因子的贡献换算回绝对值：&lt;/p&gt;&lt;span&gt;&lt;span&gt;&lt;span&gt;ΔYxi=L(Y1,Y0)⋅ln⁡xi1xi0,L(a,b)=a−bln⁡a−ln⁡b\Delta Y_{x_i} = L(Y^1, Y^0)\cdot\ln\frac{x_i^1}{x_i^0},\quad L(a,b)=\frac{a-b}{\ln a-\ln b}&lt;/span&gt;&lt;span&gt;&lt;span&gt;&lt;span&gt;&lt;/span&gt;&lt;span&gt;Δ&lt;/span&gt;&lt;span&gt;&lt;span&gt;Y&lt;/span&gt;&lt;span&gt;&lt;span&gt;&lt;span&gt;&lt;span&gt;&lt;span&gt;&lt;span&gt;&lt;/span&gt;&lt;span&gt;&lt;span&gt;&lt;span&gt;&lt;span&gt;x&lt;/span&gt;&lt;span&gt;&lt;span&gt;&lt;span&gt;&lt;span&gt;&lt;span&gt;&lt;span&gt;&lt;/span&gt;&lt;span&gt;&lt;span&gt;i&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span&gt;​&lt;/span&gt;&lt;/span&gt;&lt;span&gt;&lt;span&gt;&lt;span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span&gt;​&lt;/span&gt;&lt;/span&gt;&lt;span&gt;&lt;span&gt;&lt;span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span&gt;&lt;/span&gt;&lt;span&gt;=&lt;/span&gt;&lt;span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span&gt;&lt;span&gt;&lt;/span&gt;&lt;span&gt;L&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;&lt;span&gt;Y&lt;/span&gt;&lt;span&gt;&lt;span&gt;&lt;span&gt;&lt;span&gt;&lt;span&gt;&lt;span&gt;&lt;/span&gt;&lt;span&gt;&lt;span&gt;1&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span&gt;,&lt;/span&gt;&lt;span&gt;&lt;/span&gt;&lt;span&gt;&lt;span&gt;Y&lt;/span&gt;&lt;span&gt;&lt;span&gt;&lt;span&gt;&lt;span&gt;&lt;span&gt;&lt;span&gt;&lt;/span&gt;&lt;span&gt;&lt;span&gt;0&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span&gt;)&lt;/span&gt;&lt;span&gt;&lt;/span&gt;&lt;span&gt;⋅&lt;/span&gt;&lt;span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span&gt;&lt;span&gt;&lt;/span&gt;&lt;span&gt;ln&lt;/span&gt;&lt;span&gt;&lt;/span&gt;&lt;span&gt;&lt;span&gt;&lt;/span&gt;&lt;span&gt;&lt;span&gt;&lt;span&gt;&lt;span&gt;&lt;span&gt;&lt;span&gt;&lt;/span&gt;&lt;span&gt;&lt;span&gt;&lt;span&gt;x&lt;/span&gt;&lt;span&gt;&lt;span&gt;&lt;span&gt;&lt;span&gt;&lt;span&gt;&lt;span&gt;&lt;/span&gt;&lt;span&gt;&lt;span&gt;i&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span&gt;&lt;span&gt;&lt;/span&gt;&lt;span&gt;&lt;span&gt;0&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span&gt;​&lt;/span&gt;&lt;/span&gt;&lt;span&gt;&lt;span&gt;&lt;span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span&gt;&lt;span&gt;&lt;/span&gt;&lt;span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span&gt;&lt;span&gt;&lt;/span&gt;&lt;span&gt;&lt;span&gt;&lt;span&gt;x&lt;/span&gt;&lt;span&gt;&lt;span&gt;&lt;span&gt;&lt;span&gt;&lt;span&gt;&lt;span&gt;&lt;/span&gt;&lt;span&gt;&lt;span&gt;i&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span&gt;&lt;span&gt;&lt;/span&gt;&lt;span&gt;&lt;span&gt;1&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span&gt;​&lt;/span&gt;&lt;/span&gt;&lt;span&gt;&lt;span&gt;&lt;span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span&gt;​&lt;/span&gt;&lt;/span&gt;&lt;span&gt;&lt;span&gt;&lt;span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span&gt;,&lt;/span&gt;&lt;span&gt;&lt;/span&gt;&lt;span&gt;&lt;/span&gt;&lt;span&gt;L&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;a&lt;/span&gt;&lt;span&gt;,&lt;/span&gt;&lt;span&gt;&lt;/span&gt;&lt;span&gt;b&lt;/span&gt;&lt;span&gt;)&lt;/span&gt;&lt;span&gt;&lt;/span&gt;&lt;span&gt;=&lt;/span&gt;&lt;span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span&gt;&lt;span&gt;&lt;/span&gt;&lt;span&gt;&lt;span&gt;&lt;/span&gt;&lt;span&gt;&lt;span&gt;&lt;span&gt;&lt;span&gt;&lt;span&gt;&lt;span&gt;&lt;/span&gt;&lt;span&gt;&lt;span&gt;ln&lt;/span&gt;&lt;span&gt;&lt;/span&gt;&lt;span&gt;a&lt;/span&gt;&lt;span&gt;&lt;/span&gt;&lt;span&gt;−&lt;/span&gt;&lt;span&gt;&lt;/span&gt;&lt;span&gt;ln&lt;/span&gt;&lt;span&gt;&lt;/span&gt;&lt;span&gt;b&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span&gt;&lt;span&gt;&lt;/span&gt;&lt;span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span&gt;&lt;span&gt;&lt;/span&gt;&lt;span&gt;&lt;span&gt;a&lt;/span&gt;&lt;span&gt;&lt;/span&gt;&lt;span&gt;−&lt;/span&gt;&lt;span&gt;&lt;/span&gt;&lt;span&gt;b&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span&gt;​&lt;/span&gt;&lt;/span&gt;&lt;span&gt;&lt;span&gt;&lt;span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;p&gt;这种分解没有残差，各因子贡献加起来正好等于总变化。&lt;/p&gt;&lt;p&gt;&lt;strong&gt;例子&lt;/strong&gt;：上月计费带宽 1000、单价 10，成本 10000；本月带宽 1200、单价 9，成本 10800，涨了 800。&lt;/p&gt;&lt;ul&gt;
&lt;li&gt;对数平均权重 L ≈ 10395&lt;/li&gt;
&lt;li&gt;带宽的贡献 ≈ 10395 × ln(1.2) ≈ &lt;strong&gt;+1895&lt;/strong&gt;&lt;/li&gt;
&lt;li&gt;单价的贡献 ≈ 10395 × ln(0.9) ≈ &lt;strong&gt;-1095&lt;/strong&gt;&lt;/li&gt;
&lt;li&gt;合计 = 800 ✓&lt;/li&gt;
&lt;/ul&gt;&lt;p&gt;结论很清楚：成本上涨是用量增长带来的，降价抵消了一部分。&lt;/p&gt;&lt;/section&gt;
&lt;section&gt;&lt;h2&gt;四、比率型指标：要分清「自己变了」还是「结构变了」&lt;a href=&quot;#四比率型指标要分清自己变了还是结构变了&quot;&gt;&lt;span&gt;#&lt;/span&gt;&lt;/a&gt;&lt;/h2&gt;&lt;p&gt;这是最容易算错、也最有价值的一类。&lt;/p&gt;&lt;p&gt;&lt;strong&gt;形态&lt;/strong&gt;：指标 = 分子 / 分母。比如毛利率、命中率、单位带宽成本。整体比率可以写成各组比率按分母占比的加权平均：&lt;/p&gt;&lt;span&gt;&lt;span&gt;&lt;span&gt;r=∑iri pi,pi=第 i 组分母占比r = \sum_i r_i\, p_i,\quad p_i=\text{第 } i \text{ 组分母占比}&lt;/span&gt;&lt;span&gt;&lt;span&gt;&lt;span&gt;&lt;/span&gt;&lt;span&gt;r&lt;/span&gt;&lt;span&gt;&lt;/span&gt;&lt;span&gt;=&lt;/span&gt;&lt;span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span&gt;&lt;span&gt;&lt;/span&gt;&lt;span&gt;&lt;span&gt;&lt;span&gt;&lt;span&gt;&lt;span&gt;&lt;span&gt;&lt;/span&gt;&lt;span&gt;&lt;span&gt;i&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span&gt;&lt;span&gt;&lt;/span&gt;&lt;span&gt;&lt;span&gt;∑&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span&gt;​&lt;/span&gt;&lt;/span&gt;&lt;span&gt;&lt;span&gt;&lt;span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span&gt;&lt;/span&gt;&lt;span&gt;&lt;span&gt;r&lt;/span&gt;&lt;span&gt;&lt;span&gt;&lt;span&gt;&lt;span&gt;&lt;span&gt;&lt;span&gt;&lt;/span&gt;&lt;span&gt;&lt;span&gt;i&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span&gt;​&lt;/span&gt;&lt;/span&gt;&lt;span&gt;&lt;span&gt;&lt;span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span&gt;&lt;/span&gt;&lt;span&gt;&lt;span&gt;p&lt;/span&gt;&lt;span&gt;&lt;span&gt;&lt;span&gt;&lt;span&gt;&lt;span&gt;&lt;span&gt;&lt;/span&gt;&lt;span&gt;&lt;span&gt;i&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span&gt;​&lt;/span&gt;&lt;/span&gt;&lt;span&gt;&lt;span&gt;&lt;span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span&gt;,&lt;/span&gt;&lt;span&gt;&lt;/span&gt;&lt;span&gt;&lt;/span&gt;&lt;span&gt;&lt;span&gt;p&lt;/span&gt;&lt;span&gt;&lt;span&gt;&lt;span&gt;&lt;span&gt;&lt;span&gt;&lt;span&gt;&lt;/span&gt;&lt;span&gt;&lt;span&gt;i&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span&gt;​&lt;/span&gt;&lt;/span&gt;&lt;span&gt;&lt;span&gt;&lt;span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span&gt;&lt;/span&gt;&lt;span&gt;=&lt;/span&gt;&lt;span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span&gt;&lt;span&gt;&lt;/span&gt;&lt;span&gt;&lt;span&gt;第&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;/span&gt;&lt;span&gt;i&lt;/span&gt;&lt;span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;组分母占比&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;p&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;结构变化&lt;/strong&gt;：各组本身没变，但低毛利客户的收入占比变大了，整体也会被拉低。&lt;/li&gt;
&lt;/ul&gt;&lt;p&gt;如果只看各客户自身的毛利率，会完全漏掉第二种原因。&lt;/p&gt;&lt;section&gt;&lt;h3&gt;三种算法&lt;a href=&quot;#三种算法&quot;&gt;&lt;span&gt;#&lt;/span&gt;&lt;/a&gt;&lt;/h3&gt;&lt;p&gt;&lt;strong&gt;1. 控制变量法&lt;/strong&gt;：只让一组变化，其他组保持基期，看整体比率变了多少。直观，但各组贡献加起来通常不等于总变化，不推荐。&lt;/p&gt;&lt;p&gt;&lt;strong&gt;2. 综合占比法&lt;/strong&gt;：把变化拆成「组内比率变化」和「占比变化」两部分。各组贡献可以相加，等于总变化。&lt;/p&gt;&lt;p&gt;&lt;strong&gt;3. 综合贡献法&lt;/strong&gt;（推荐）：在综合占比法基础上，再把结构项改成「该组比率与整体比率的差距 × 占比变化」：&lt;/p&gt;&lt;span&gt;&lt;span&gt;&lt;span&gt;Δr=∑i[Δri pi0⏟组内变化+Δri Δpi⏟交叉项+(ri0−r0) Δpi⏟结构变化]\Delta r = \sum_i \Big[\underbrace{\Delta r_i\, p_i^0}_{\text{组内变化}} + \underbrace{\Delta r_i\, \Delta p_i}_{\text{交叉项}} + \underbrace{(r_i^0 - r^0)\,\Delta p_i}_{\text{结构变化}}\Big]&lt;/span&gt;&lt;span&gt;&lt;span&gt;&lt;span&gt;&lt;/span&gt;&lt;span&gt;Δ&lt;/span&gt;&lt;span&gt;r&lt;/span&gt;&lt;span&gt;&lt;/span&gt;&lt;span&gt;=&lt;/span&gt;&lt;span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span&gt;&lt;span&gt;&lt;/span&gt;&lt;span&gt;&lt;span&gt;&lt;span&gt;&lt;span&gt;&lt;span&gt;&lt;span&gt;&lt;/span&gt;&lt;span&gt;&lt;span&gt;i&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span&gt;&lt;span&gt;&lt;/span&gt;&lt;span&gt;&lt;span&gt;∑&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span&gt;​&lt;/span&gt;&lt;/span&gt;&lt;span&gt;&lt;span&gt;&lt;span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span&gt;&lt;/span&gt;&lt;span&gt;&lt;span&gt;[&lt;/span&gt;&lt;/span&gt;&lt;span&gt;&lt;/span&gt;&lt;span&gt;&lt;span&gt;&lt;span&gt;&lt;span&gt;&lt;span&gt;&lt;span&gt;&lt;/span&gt;&lt;span&gt;&lt;span&gt;&lt;span&gt;&lt;span&gt;组内变化&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span&gt;&lt;span&gt;&lt;/span&gt;&lt;span&gt;&lt;span&gt;&lt;span&gt;&lt;span&gt;&lt;span&gt;&lt;span&gt;&lt;/span&gt;&lt;span&gt;&lt;span&gt;&lt;/span&gt;&lt;span&gt;&lt;/span&gt;&lt;span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span&gt;&lt;span&gt;&lt;/span&gt;&lt;span&gt;&lt;span&gt;Δ&lt;/span&gt;&lt;span&gt;&lt;span&gt;r&lt;/span&gt;&lt;span&gt;&lt;span&gt;&lt;span&gt;&lt;span&gt;&lt;span&gt;&lt;span&gt;&lt;/span&gt;&lt;span&gt;&lt;span&gt;i&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span&gt;​&lt;/span&gt;&lt;/span&gt;&lt;span&gt;&lt;span&gt;&lt;span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span&gt;&lt;/span&gt;&lt;span&gt;&lt;span&gt;p&lt;/span&gt;&lt;span&gt;&lt;span&gt;&lt;span&gt;&lt;span&gt;&lt;span&gt;&lt;span&gt;&lt;/span&gt;&lt;span&gt;&lt;span&gt;i&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span&gt;&lt;span&gt;&lt;/span&gt;&lt;span&gt;&lt;span&gt;0&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span&gt;​&lt;/span&gt;&lt;/span&gt;&lt;span&gt;&lt;span&gt;&lt;span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span&gt;​&lt;/span&gt;&lt;/span&gt;&lt;span&gt;&lt;span&gt;&lt;span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span&gt;​&lt;/span&gt;&lt;/span&gt;&lt;span&gt;&lt;span&gt;&lt;span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span&gt;&lt;/span&gt;&lt;span&gt;+&lt;/span&gt;&lt;span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span&gt;&lt;span&gt;&lt;/span&gt;&lt;span&gt;&lt;span&gt;&lt;span&gt;&lt;span&gt;&lt;span&gt;&lt;span&gt;&lt;/span&gt;&lt;span&gt;&lt;span&gt;&lt;span&gt;&lt;span&gt;交叉项&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span&gt;&lt;span&gt;&lt;/span&gt;&lt;span&gt;&lt;span&gt;&lt;span&gt;&lt;span&gt;&lt;span&gt;&lt;span&gt;&lt;/span&gt;&lt;span&gt;&lt;span&gt;&lt;/span&gt;&lt;span&gt;&lt;/span&gt;&lt;span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span&gt;&lt;span&gt;&lt;/span&gt;&lt;span&gt;&lt;span&gt;Δ&lt;/span&gt;&lt;span&gt;&lt;span&gt;r&lt;/span&gt;&lt;span&gt;&lt;span&gt;&lt;span&gt;&lt;span&gt;&lt;span&gt;&lt;span&gt;&lt;/span&gt;&lt;span&gt;&lt;span&gt;i&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span&gt;​&lt;/span&gt;&lt;/span&gt;&lt;span&gt;&lt;span&gt;&lt;span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span&gt;&lt;/span&gt;&lt;span&gt;Δ&lt;/span&gt;&lt;span&gt;&lt;span&gt;p&lt;/span&gt;&lt;span&gt;&lt;span&gt;&lt;span&gt;&lt;span&gt;&lt;span&gt;&lt;span&gt;&lt;/span&gt;&lt;span&gt;&lt;span&gt;i&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span&gt;​&lt;/span&gt;&lt;/span&gt;&lt;span&gt;&lt;span&gt;&lt;span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span&gt;​&lt;/span&gt;&lt;/span&gt;&lt;span&gt;&lt;span&gt;&lt;span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span&gt;​&lt;/span&gt;&lt;/span&gt;&lt;span&gt;&lt;span&gt;&lt;span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span&gt;&lt;/span&gt;&lt;span&gt;+&lt;/span&gt;&lt;span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span&gt;&lt;span&gt;&lt;/span&gt;&lt;span&gt;&lt;span&gt;&lt;span&gt;&lt;span&gt;&lt;span&gt;&lt;span&gt;&lt;/span&gt;&lt;span&gt;&lt;span&gt;&lt;span&gt;&lt;span&gt;结构变化&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span&gt;&lt;span&gt;&lt;/span&gt;&lt;span&gt;&lt;span&gt;&lt;span&gt;&lt;span&gt;&lt;span&gt;&lt;span&gt;&lt;/span&gt;&lt;span&gt;&lt;span&gt;&lt;/span&gt;&lt;span&gt;&lt;/span&gt;&lt;span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span&gt;&lt;span&gt;&lt;/span&gt;&lt;span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;&lt;span&gt;r&lt;/span&gt;&lt;span&gt;&lt;span&gt;&lt;span&gt;&lt;span&gt;&lt;span&gt;&lt;span&gt;&lt;/span&gt;&lt;span&gt;&lt;span&gt;i&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span&gt;&lt;span&gt;&lt;/span&gt;&lt;span&gt;&lt;span&gt;0&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span&gt;​&lt;/span&gt;&lt;/span&gt;&lt;span&gt;&lt;span&gt;&lt;span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span&gt;&lt;/span&gt;&lt;span&gt;−&lt;/span&gt;&lt;span&gt;&lt;/span&gt;&lt;span&gt;&lt;span&gt;r&lt;/span&gt;&lt;span&gt;&lt;span&gt;&lt;span&gt;&lt;span&gt;&lt;span&gt;&lt;span&gt;&lt;/span&gt;&lt;span&gt;&lt;span&gt;0&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span&gt;)&lt;/span&gt;&lt;span&gt;&lt;/span&gt;&lt;span&gt;Δ&lt;/span&gt;&lt;span&gt;&lt;span&gt;p&lt;/span&gt;&lt;span&gt;&lt;span&gt;&lt;span&gt;&lt;span&gt;&lt;span&gt;&lt;span&gt;&lt;/span&gt;&lt;span&gt;&lt;span&gt;i&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span&gt;​&lt;/span&gt;&lt;/span&gt;&lt;span&gt;&lt;span&gt;&lt;span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span&gt;​&lt;/span&gt;&lt;/span&gt;&lt;span&gt;&lt;span&gt;&lt;span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span&gt;​&lt;/span&gt;&lt;/span&gt;&lt;span&gt;&lt;span&gt;&lt;span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span&gt;&lt;/span&gt;&lt;span&gt;&lt;span&gt;]&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;p&gt;推导的关键一步是：各组占比之和恒为 1，所以占比变化之和为 0，可以在结构项里减去一个 r⁰ × ΣΔpᵢ 而不改变总和。这样改写之后，结构项的含义变成了：&lt;strong&gt;一个比平均水平差的组占比变大，才会拉低整体&lt;/strong&gt;，解释起来顺多了。&lt;/p&gt;&lt;/section&gt;&lt;section&gt;&lt;h3&gt;例子：毛利率掉了 3 个点&lt;a href=&quot;#例子毛利率掉了-3-个点&quot;&gt;&lt;span&gt;#&lt;/span&gt;&lt;/a&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;th&gt;本月毛利率&lt;/th&gt;&lt;/tr&gt;&lt;/thead&gt;&lt;tbody&gt;&lt;tr&gt;&lt;td&gt;A&lt;/td&gt;&lt;td&gt;60%&lt;/td&gt;&lt;td&gt;40%&lt;/td&gt;&lt;td&gt;50%&lt;/td&gt;&lt;td&gt;40%&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;B&lt;/td&gt;&lt;td&gt;40%&lt;/td&gt;&lt;td&gt;20%&lt;/td&gt;&lt;td&gt;50%&lt;/td&gt;&lt;td&gt;18%&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;整体&lt;/td&gt;&lt;td&gt;&lt;/td&gt;&lt;td&gt;32%&lt;/td&gt;&lt;td&gt;&lt;/td&gt;&lt;td&gt;29%&lt;/td&gt;&lt;/tr&gt;&lt;/tbody&gt;&lt;/table&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;th&gt;合计&lt;/th&gt;&lt;/tr&gt;&lt;/thead&gt;&lt;tbody&gt;&lt;tr&gt;&lt;td&gt;A&lt;/td&gt;&lt;td&gt;0&lt;/td&gt;&lt;td&gt;0&lt;/td&gt;&lt;td&gt;-0.8 pp&lt;/td&gt;&lt;td&gt;-0.8 pp&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;B&lt;/td&gt;&lt;td&gt;-0.8 pp&lt;/td&gt;&lt;td&gt;-0.2 pp&lt;/td&gt;&lt;td&gt;-1.2 pp&lt;/td&gt;&lt;td&gt;-2.2 pp&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;合计&lt;/td&gt;&lt;td&gt;-0.8 pp&lt;/td&gt;&lt;td&gt;-0.2 pp&lt;/td&gt;&lt;td&gt;-2.0 pp&lt;/td&gt;&lt;td&gt;&lt;strong&gt;-3.0 pp&lt;/strong&gt;&lt;/td&gt;&lt;/tr&gt;&lt;/tbody&gt;&lt;/table&gt;&lt;p&gt;结论：毛利率下降的 3 个点里，有 2 个点来自&lt;strong&gt;结构变化&lt;/strong&gt;，也就是低毛利的客户 B 收入占比上升了；只有 1 个点是 B 自身毛利率下降。如果只盯着各客户的毛利率，会以为 A 完全没问题、B 只降了 2 个点，得出错误的结论。&lt;/p&gt;&lt;/section&gt;&lt;/section&gt;
&lt;section&gt;&lt;h2&gt;五、在字节的应用：按指标公式逐层递归&lt;a href=&quot;#五在字节的应用按指标公式逐层递归&quot;&gt;&lt;span&gt;#&lt;/span&gt;&lt;/a&gt;&lt;/h2&gt;&lt;p&gt;在火山 CDN 经营分析平台上，我把上面三类方法组合成了一套递归归因的逻辑。&lt;/p&gt;&lt;section&gt;&lt;h3&gt;1. 先建好指标公式树&lt;a href=&quot;#1-先建好指标公式树&quot;&gt;&lt;span&gt;#&lt;/span&gt;&lt;/a&gt;&lt;/h3&gt;&lt;p&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;收入&lt;/strong&gt;（加法）= 新增客户收入 + 存量客户收入（流失客户记为负）
&lt;ul&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;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;成本&lt;/strong&gt;（加法）= 各客户成本之和 = 各域名成本之和
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;客户成本&lt;/strong&gt;（乘法）≈ 计费用量 × 单位成本&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;&lt;p&gt;其中成本数据来自我设计的「95 区间」分摊算法，每天产出到客户、域名粒度，这是整套归因能落地的前提：没有细粒度的成本，就拆不下去。&lt;/p&gt;&lt;/section&gt;&lt;section&gt;&lt;h3&gt;2. 收入归因：客户、用量、价格&lt;a href=&quot;#2-收入归因客户用量价格&quot;&gt;&lt;span&gt;#&lt;/span&gt;&lt;/a&gt;&lt;/h3&gt;&lt;p&gt;收入是经营分析里被问得最多的指标，也是拆得最细的一棵子树。它的难点在于，同一个「收入变化」背后可能是完全不同的业务动作：新签了客户、老客户放量、续约降价、流量往海外迁移，这些对应的跟进人和应对方式都不一样。所以收入归因要分三层拆。&lt;/p&gt;&lt;p&gt;&lt;strong&gt;第一层：客户增减（加法）&lt;/strong&gt;&lt;/p&gt;&lt;p&gt;先把收入变化按客户类型分开：&lt;/p&gt;&lt;ul&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;/ul&gt;&lt;p&gt;例如本月收入环比 +50 万：新增客户 +30 万，流失客户 -40 万，存量客户 +60 万。单看总数是小幅增长，拆开才发现流失其实很严重，只是被存量增长盖住了。&lt;/p&gt;&lt;p&gt;&lt;strong&gt;第二层：用量 × 单价（乘法，LMDI）&lt;/strong&gt;&lt;/p&gt;&lt;p&gt;存量客户的收入 = 计费用量 × 平均单价，用前面的 LMDI 拆成「用量变化」和「价格变化」两部分。&lt;/p&gt;&lt;p&gt;例如头部客户 C，上月收入 200 万，本月用量 +20%，续约后单价 -10%，收入变成 216 万：&lt;/p&gt;&lt;ul&gt;
&lt;li&gt;对数平均权重 L ≈ 207.9&lt;/li&gt;
&lt;li&gt;用量贡献 ≈ 207.9 × ln(1.2) ≈ &lt;strong&gt;+37.9 万&lt;/strong&gt;&lt;/li&gt;
&lt;li&gt;价格贡献 ≈ 207.9 × ln(0.9) ≈ &lt;strong&gt;-21.9 万&lt;/strong&gt;&lt;/li&gt;
&lt;li&gt;合计 = +16 万 ✓&lt;/li&gt;
&lt;/ul&gt;&lt;p&gt;这个结论对销售和运营很有用：C 的业务在增长，但降价吃掉了一半以上的增量。&lt;/p&gt;&lt;p&gt;&lt;strong&gt;第三层：单价变化的「真调价」与「结构变化」（比率）&lt;/strong&gt;&lt;/p&gt;&lt;p&gt;拆到价格还没完。CDN 的平均单价本身是一个比率：不同计费区域（国内、海外各区域）、不同计费项的单价差别很大，平均单价 = 各项单价按用量占比加权。所以平均单价变了，可能根本没有调价，只是用量结构变了。&lt;/p&gt;&lt;p&gt;例如某客户国内单价 10、海外单价 30，两个月都没调价：&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;90%&lt;/td&gt;&lt;td&gt;80%&lt;/td&gt;&lt;td&gt;10&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;海外&lt;/td&gt;&lt;td&gt;10%&lt;/td&gt;&lt;td&gt;20%&lt;/td&gt;&lt;td&gt;30&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;12 → 14&lt;/td&gt;&lt;/tr&gt;&lt;/tbody&gt;&lt;/table&gt;&lt;p&gt;用综合贡献法拆，组内变化（真调价）为 0，平均单价上涨的 2 全部来自结构变化：海外占比上升贡献 +1.8，国内占比下降贡献 +0.2。也就是说，这个客户的「涨价」其实是流量往海外迁移了。如果把它当成调价成功去汇报，就是错误的结论。&lt;/p&gt;&lt;p&gt;到这里，一次收入异动就能落到具体的业务动作上：是哪类客户、哪个客户，是用量还是价格，价格是真调价还是区域结构变化。&lt;/p&gt;&lt;/section&gt;&lt;section&gt;&lt;h3&gt;3. 递归下钻&lt;a href=&quot;#3-递归下钻&quot;&gt;&lt;span&gt;#&lt;/span&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;算出各子节点的贡献度，取贡献最大的那个；&lt;/li&gt;
&lt;li&gt;对这个子节点重复上面的过程，直到叶子节点，或者贡献度已经足够分散、无法再明显收敛。&lt;/li&gt;
&lt;/ol&gt;&lt;p&gt;最后输出一条归因路径，例如：&lt;/p&gt;&lt;blockquote&gt;&lt;p&gt;毛利率环比 -3pp → 主要来自结构变化（低毛利客户收入占比上升）→ 客户 B → 成本上涨 → 计费用量增长贡献最大 → 域名 x&lt;/p&gt;&lt;/blockquote&gt;&lt;blockquote&gt;&lt;p&gt;收入环比 +50 万 → 存量客户 +60 万、流失客户 -40 万 → 存量中客户 C 贡献最大 → 用量 +37.9 万、价格 -21.9 万 → 价格下降来自续约调价&lt;/p&gt;&lt;/blockquote&gt;&lt;p&gt;这样，看板上看到的不再只是一个变红的数字，而是一句可以直接拿去跟进的结论。&lt;/p&gt;&lt;/section&gt;&lt;section&gt;&lt;h3&gt;4. 实践里的几点体会&lt;a href=&quot;#4-实践里的几点体会&quot;&gt;&lt;span&gt;#&lt;/span&gt;&lt;/a&gt;&lt;/h3&gt;&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;公式要和口径完全一致&lt;/strong&gt;：归因拆出来的子指标，必须和看板上的计算口径一致，否则各部分加起来对不上总数，业务方第一眼就不信。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;比率指标一定要看结构&lt;/strong&gt;：CDN 业务里客户结构变化非常常见，比如某个大客户放量、低价客户占比上升，只看组内变化很容易误判。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;新增和流失要单独处理&lt;/strong&gt;：新客户上期为 0、流失客户本期为 0，取对数会出错，LMDI 也算不了，必须在乘法拆解之前先用加法把它们拆出去。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;递归要有停止条件&lt;/strong&gt;：贡献度太分散时，继续下钻只会得到噪音；这时输出「多因素共同影响」比强行给出一个结论更可靠。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;归因只是定位，不是解释&lt;/strong&gt;：它能告诉你「是 B 客户的用量涨了」，但「为什么涨」还需要运营去跟客户确认。&lt;/li&gt;
&lt;/ul&gt;&lt;/section&gt;&lt;/section&gt;
&lt;section&gt;&lt;h2&gt;参考&lt;a href=&quot;#参考&quot;&gt;&lt;span&gt;#&lt;/span&gt;&lt;/a&gt;&lt;/h2&gt;&lt;ul&gt;
&lt;li&gt;&lt;a href=&quot;https://damodev.csdn.net/68243c99a5baf817cf4bc552.html&quot; target=&quot;_blank&quot;&gt;指标归因贡献度计算方法（加法 / 乘法 / 除法型）- CSDN&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;Ang, B.W. (2005). The LMDI approach to decomposition analysis: a practical guide. &lt;em&gt;Energy Policy&lt;/em&gt;, 33(7), 867–871.&lt;/li&gt;
&lt;/ul&gt;&lt;/section&gt;</content:encoded></item><item><title>我的第一项专利：用「95 区间」把 CDN 带宽成本分得更准</title><link>https://tracerzzz.cn/posts/cdn-bandwidth-cost-allocation-patent/</link><guid isPermaLink="true">https://tracerzzz.cn/posts/cdn-bandwidth-cost-allocation-patent/</guid><description>白话解读我作为第一发明人的发明专利（中国 CN117061354B、美国 US12375742B2）：为什么按 95 峰值时间点分摊 CDN 带宽成本会算不准，以及「95 区间」分摊法是怎么解决的。附专利原文 PDF。</description><pubDate>Sun, 25 Oct 2026 05:00:00 GMT</pubDate><content:encoded>&lt;p&gt;这是我在字节跳动做 CDN 成本分摊时设计的一套算法，后来申请了专利，并在中国和美国都拿到了授权。我是第一发明人。这篇文章不讲法律文本，用大白话说清楚它解决了什么问题、是怎么做的。&lt;/p&gt;
&lt;section&gt;&lt;h2&gt;专利信息&lt;a href=&quot;#专利信息&quot;&gt;&lt;span&gt;#&lt;/span&gt;&lt;/a&gt;&lt;/h2&gt;

&lt;table&gt;&lt;thead&gt;&lt;tr&gt;&lt;th&gt;&lt;/th&gt;&lt;th&gt;中国&lt;/th&gt;&lt;th&gt;美国&lt;/th&gt;&lt;/tr&gt;&lt;/thead&gt;&lt;tbody&gt;&lt;tr&gt;&lt;td&gt;名称&lt;/td&gt;&lt;td&gt;CDN中的信息处理方法、系统、设备和存储介质&lt;/td&gt;&lt;td&gt;Method and system for processing information in CDN, device, and storage medium&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;专利号&lt;/td&gt;&lt;td&gt;CN117061354B&lt;/td&gt;&lt;td&gt;US12375742B2&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;申请日&lt;/td&gt;&lt;td&gt;2023-10-10&lt;/td&gt;&lt;td&gt;2024-10-10&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;授权日&lt;/td&gt;&lt;td&gt;2024-01-12&lt;/td&gt;&lt;td&gt;2025-07-29&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;专利权人&lt;/td&gt;&lt;td&gt;北京火山引擎科技有限公司&lt;/td&gt;&lt;td&gt;Beijing Volcano Engine Technology Co., Ltd.&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;原文&lt;/td&gt;&lt;td&gt;&lt;a href=&quot;/files/patents/CN117061354B.pdf&quot;&gt;下载 PDF&lt;/a&gt;&lt;/td&gt;&lt;td&gt;&lt;a href=&quot;/files/patents/US20250119603A1.pdf&quot;&gt;下载公开文本 PDF&lt;/a&gt; · &lt;a href=&quot;https://image-ppubs.uspto.gov/dirsearch-public/print/downloadPdf/12375742&quot; target=&quot;_blank&quot;&gt;USPTO 授权全文&lt;/a&gt;&lt;/td&gt;&lt;/tr&gt;&lt;/tbody&gt;&lt;/table&gt;&lt;/section&gt;
&lt;section&gt;&lt;h2&gt;一、问题：一份带宽账单，怎么分给很多用户？&lt;a href=&quot;#一问题一份带宽账单怎么分给很多用户&quot;&gt;&lt;span&gt;#&lt;/span&gt;&lt;/a&gt;&lt;/h2&gt;&lt;p&gt;CDN 厂商（或者一家公司内部采购 CDN 的平台）买的是一整份带宽，再给很多用户、很多域名共享着用。月底付给上游的带宽费用，业内最常见的计费方式是 &lt;strong&gt;95 峰值&lt;/strong&gt;：&lt;/p&gt;&lt;blockquote&gt;&lt;p&gt;一个月里每 5 分钟采一个总带宽点，30 天共 8640 个点。从大到小排序，去掉最高的 5%（432 个点），剩下的最大值，也就是第 433 个点，就是「95 带宽值」，按它计费。&lt;/p&gt;&lt;/blockquote&gt;&lt;p&gt;钱是按总带宽付的，接下来要回答的问题是：&lt;strong&gt;这笔钱里，每个用户该承担多少？&lt;/strong&gt; 这就是成本分摊，它直接决定了每个客户的毛利、每个内部业务该被收多少钱。&lt;/p&gt;&lt;/section&gt;
&lt;section&gt;&lt;h2&gt;二、传统做法：只看 95 那一个时间点&lt;a href=&quot;#二传统做法只看-95-那一个时间点&quot;&gt;&lt;span&gt;#&lt;/span&gt;&lt;/a&gt;&lt;/h2&gt;&lt;p&gt;常见的做法很直接：找到 95 带宽值对应的那个时间点（「95 时间点」），看每个用户在这个时间点用了多少带宽，按比例分。&lt;/p&gt;&lt;p&gt;比如 95 带宽值是 300，用户 A 在 95 时间点用了 100，那 A 就承担三分之一的成本。&lt;/p&gt;&lt;p&gt;简单，但有两个硬伤。&lt;/p&gt;&lt;p&gt;&lt;strong&gt;1. 只看一个点，不能代表整个月。&lt;/strong&gt; 假设 95 时间点落在第 15 天，用户 A 那一刻恰好没用带宽，但第 16 到 25 天用得很猛。按这种算法，A 一分钱不用分摊，可实际上正是 A 这些天的高用量把整体带宽和成本推高了。&lt;/p&gt;&lt;p&gt;&lt;strong&gt;2. 选哪个点，结果就不一样。&lt;/strong&gt; 总带宽曲线一天里通常有早晚两个高峰，一条水平线（95 带宽值）会和曲线交出好几个点，这几个点的总带宽都等于 95 值。随便挑一个，分摊结果可能天差地别：在 a 点 A 没用带宽，不用分摊；换成 b 点 A 用了，就要分摊。同一份账单，结果取决于运气。&lt;/p&gt;&lt;/section&gt;
&lt;section&gt;&lt;h2&gt;三、我的做法：把「一个点」换成「一段区间」&lt;a href=&quot;#三我的做法把一个点换成一段区间&quot;&gt;&lt;span&gt;#&lt;/span&gt;&lt;/a&gt;&lt;/h2&gt;&lt;p&gt;核心想法一句话：&lt;strong&gt;不看单个时间点，而是看 95 值附近的一段带宽区间里，各用户整体用了多少。&lt;/strong&gt;&lt;/p&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;：在 95 带宽值附近定一个下界 A 和一个上界 B（比如 95 值上下各取一个总带宽值），总带宽曲线落在 A 到 B 之间的那些时间段，就是「管理区间」。一个月里通常会有很多段。&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;p&gt;下面是专利中的图 7，说明管理区间是怎么划出来的：&lt;/p&gt;&lt;p&gt;&lt;/p&gt;&lt;figure&gt;&lt;img alt=&quot;专利图 7：在 95 带宽值上下取阈值 A、B，总带宽曲线落在两条虚线之间的时间段 a1–a2、b1–b2、c1–c2、d1–d2 就是管理区间&quot; loading=&quot;lazy&quot; width=&quot;959&quot; height=&quot;545&quot; src=&quot;/_astro/fig7-management-interval.mS-7XHW6_Z2pfm9E.webp&quot; /&gt;&lt;figcaption&gt;专利图 7：在 95 带宽值上下取阈值 A、B，总带宽曲线落在两条虚线之间的时间段 a1–a2、b1–b2、c1–c2、d1–d2 就是管理区间&lt;/figcaption&gt;&lt;/figure&gt;&lt;p&gt;&lt;/p&gt;&lt;p&gt;95 带宽值是图中的实线，A、B 是它下方和上方的两个阈值。总带宽曲线每次穿过 A、B 之间这条带，就形成一段管理区间。一天有早晚两个高峰，每个高峰的上坡和下坡各穿过一次，所以这里有 a1–a2、b1–b2、c1–c2、d1–d2 四段；一个月下来，就是几十上百段。&lt;/p&gt;&lt;p&gt;第 3 步有两种算法，可以按需选择：&lt;/p&gt;&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;累加法&lt;/strong&gt;：直接把区间内各采样点的带宽值加起来。用户 A 的占比 = A 在区间内各点带宽之和 ÷ 区间内各点总带宽之和。计算简单，省算力，工程上最常用。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;积分法&lt;/strong&gt;：把总带宽和每个用户的带宽分别拟合成曲线，用区间内曲线下的面积来算占比。精度更高。&lt;/li&gt;
&lt;/ul&gt;&lt;p&gt;专利中的图 6 把积分法画得很直观：&lt;/p&gt;&lt;p&gt;&lt;/p&gt;&lt;figure&gt;&lt;img alt=&quot;专利图 6：用户 A、B、C 的带宽依次叠加成三条曲线，竖条是管理区间，每条竖条里不同灰度的面积就是各用户在区间内的用量&quot; loading=&quot;lazy&quot; width=&quot;1218&quot; height=&quot;710&quot; src=&quot;/_astro/fig6-area-share.Bn8DGAao_1rPF1Q.webp&quot; /&gt;&lt;figcaption&gt;专利图 6：用户 A、B、C 的带宽依次叠加成三条曲线，竖条是管理区间，每条竖条里不同灰度的面积就是各用户在区间内的用量&lt;/figcaption&gt;&lt;/figure&gt;&lt;p&gt;&lt;/p&gt;&lt;ul&gt;
&lt;li&gt;最下面的曲线是用户 A 的带宽；中间是 A 和 B 叠加；最上面是 A、B、C 三者叠加，也就是总带宽；&lt;/li&gt;
&lt;li&gt;顶部横着的浅灰色带，就是 95 值附近的阈值带，竖向的四根条带就是管理区间；&lt;/li&gt;
&lt;li&gt;在每根条带里，从下往上三种灰度，分别是 A、B、C 在这段时间里的用量（曲线下的面积）；&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;p&gt;另外还有一个约束：区间内的采样点数量，占整个周期采样点的比例不能低于某个阈值。区间太窄、点太少，结果又会退化成「只看几个点」。&lt;/p&gt;&lt;/section&gt;
&lt;section&gt;&lt;h2&gt;四、为什么这样更准&lt;a href=&quot;#四为什么这样更准&quot;&gt;&lt;span&gt;#&lt;/span&gt;&lt;/a&gt;&lt;/h2&gt;&lt;p&gt;回到前面的两个例子：&lt;/p&gt;&lt;ul&gt;
&lt;li&gt;用户 A 在 95 时间点没用带宽，但在区间里其他时间点用了：传统做法判定 A 不用分摊，区间法会把 A 的用量算进去。&lt;/li&gt;
&lt;li&gt;有好几个点都等于 95 值：区间法本来就把这些点都包含在内，不存在「挑哪个点」的问题，结果是稳定的。&lt;/li&gt;
&lt;/ul&gt;&lt;p&gt;还有一个实际业务中很常见、单点法处理不好的情况：&lt;strong&gt;按流量计费的客户，以及月中才接入的客户。&lt;/strong&gt;&lt;/p&gt;&lt;p&gt;CDN 厂商和上游运营商结算带宽，通常是按所有客户叠加后的总带宽取 95 值。但下游客户的计费方式各不相同：有的客户和我们按带宽结算，有的按流量结算。按流量计费的客户，他付给我们的钱和 95 无关，可他的带宽一样叠加在总带宽曲线上，一样抬高了我们付给运营商的 95 值，理应分摊这部分成本。&lt;/p&gt;&lt;p&gt;单点法在这里很容易失真：&lt;/p&gt;&lt;ul&gt;
&lt;li&gt;一个月中才接入的客户，如果 95 时间点落在他接入之前，他在那一刻的带宽是 0，就一分钱成本都分不到；&lt;/li&gt;
&lt;li&gt;按流量计费的客户用量往往不规律，95 时间点恰好赶上他的低谷，分到的成本也会远低于实际。&lt;/li&gt;
&lt;/ul&gt;&lt;p&gt;但他们在月内的其他高峰期，确实贡献了相当一部分 95 值以上的打点。区间法覆盖的是整个月里所有的高峰区间，只要客户在这些时段有用量，就会按比例分到成本。这样算出来的客户毛利更真实，能减少「看起来很赚钱、其实在亏钱」的误判。&lt;/p&gt;&lt;p&gt;同时，区间是围绕 95 值划定的，&lt;strong&gt;保留了 95 计费的业务含义&lt;/strong&gt;：分摊的依据依然是决定账单的那段高峰期用量，而不是全天的平均用量。&lt;/p&gt;&lt;p&gt;简单说，它既延续了 95 计费的逻辑，又解决了单点分摊不准、不稳的问题，对各种计费方式、各种接入时间的客户都能给出合理的成本。&lt;/p&gt;&lt;/section&gt;
&lt;section&gt;&lt;h2&gt;五、落地情况&lt;a href=&quot;#五落地情况&quot;&gt;&lt;span&gt;#&lt;/span&gt;&lt;/a&gt;&lt;/h2&gt;&lt;p&gt;这套算法最早是我在字节做火山 CDN 经营分析时设计的。验证的方式很朴素：和数据工程团队一起，用全量客户的计费打点数据，把几种分摊方式都跑一遍，在 Grafana 上并排对比结果，用数据说服团队采用。&lt;/p&gt;&lt;p&gt;后来它的使用范围不断扩大：&lt;/p&gt;&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;火山 CDN 经营分析&lt;/strong&gt;：每天定时产出客户、域名维度的成本，进一步算出收入和毛利率。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;融合 CDN 内部结算&lt;/strong&gt;：字节内部各业务（头条、抖音点播直播等）按这套方法分摊二三十家 CDN 厂商的采购成本。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;火山多云 CDN 的 FinOps 能力&lt;/strong&gt;：作为商用功能，帮客户把多家厂商的账单拆到域名。&lt;/li&gt;
&lt;/ul&gt;&lt;p&gt;数据团队后续的带宽成本分摊，也都基于这套算法实现。&lt;/p&gt;&lt;/section&gt;
&lt;section&gt;&lt;h2&gt;六、原文下载&lt;a href=&quot;#六原文下载&quot;&gt;&lt;span&gt;#&lt;/span&gt;&lt;/a&gt;&lt;/h2&gt;&lt;ul&gt;
&lt;li&gt;中国发明专利 CN117061354B：&lt;a href=&quot;/files/patents/CN117061354B.pdf&quot;&gt;PDF&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;美国专利公开文本 US 2025/0119603 A1：&lt;a href=&quot;/files/patents/US20250119603A1.pdf&quot;&gt;PDF&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;美国授权专利 US12375742B2：&lt;a href=&quot;https://image-ppubs.uspto.gov/dirsearch-public/print/downloadPdf/12375742&quot; target=&quot;_blank&quot;&gt;USPTO 全文&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;&lt;/section&gt;</content:encoded></item><item><title>从 Hexo 迁移到 Astro：图片找回、旧链接、欢迎页重写与双站点部署</title><link>https://tracerzzz.cn/posts/hexo-to-astro-migration/</link><guid isPermaLink="true">https://tracerzzz.cn/posts/hexo-to-astro-migration/</guid><description>记录把 2016 年搭的 Hexo 博客迁移到 Astro 的全过程：从七牛找回图片、转换文章、保留旧链接、重写 SVG 动画欢迎页、搭建自动检查，部署到阿里云 ESA 和 GitHub Pages，以及从 Retypeset 换到 Firefly 主题，中间踩过的坑和背后的原理。</description><pubDate>Fri, 23 Oct 2026 02:30:00 GMT</pubDate><content:encoded>&lt;p&gt;这个博客 2016 年用 Hexo 搭起来，写到 2018 年就基本停了。这几年它悄悄坏掉了：图床域名过期，所有文章的图片都打不开；本地的 Hexo 环境早就没了，想改点东西都得先折腾半天。&lt;/p&gt;
&lt;p&gt;这次干脆整体迁到 &lt;a href=&quot;https://astro.build/&quot; target=&quot;_blank&quot;&gt;Astro&lt;/a&gt;，顺便把这些年欠下的问题一起解决。这篇文章记录整个过程，以及每一步背后的原理，方便以后自己回头查。&lt;/p&gt;
&lt;section&gt;&lt;h2&gt;一、原来的状况&lt;a href=&quot;#一原来的状况&quot;&gt;&lt;span&gt;#&lt;/span&gt;&lt;/a&gt;&lt;/h2&gt;&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;框架&lt;/strong&gt;：Hexo + NexT 主题，源文件在 &lt;code&gt;hexoWorkspace&lt;/code&gt; 仓库，生成的静态网站推到 &lt;code&gt;tracerZzz.github.io&lt;/code&gt; 仓库，由 GitHub Pages 托管。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;写作&lt;/strong&gt;：Typora 写 Markdown，图片用 PicGo 上传到七牛云，文章里引用七牛的外链。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;域名&lt;/strong&gt;：博客用 &lt;code&gt;tracerzzz.cn&lt;/code&gt;（已备案），图片用的是另一个域名，&lt;strong&gt;已经过期&lt;/strong&gt;。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;首页&lt;/strong&gt;：一个用 SVG + GSAP 补间动画做的欢迎页，点击里面的按钮才进入文章列表。动画代码当年是用 webpack 手动打包压缩的。&lt;/li&gt;
&lt;/ul&gt;&lt;p&gt;问题很明显：&lt;strong&gt;文章和图片分在两个地方，图床一挂，文章就废了一半&lt;/strong&gt;。&lt;/p&gt;&lt;/section&gt;
&lt;section&gt;&lt;h2&gt;二、目标和选型&lt;a href=&quot;#二目标和选型&quot;&gt;&lt;span&gt;#&lt;/span&gt;&lt;/a&gt;&lt;/h2&gt;&lt;ol&gt;
&lt;li&gt;&lt;strong&gt;图片跟着文章走&lt;/strong&gt;：图片和 Markdown 放在一起，存进同一个 git 仓库。不再依赖任何图床。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;旧链接不能断&lt;/strong&gt;：以前分享出去的文章地址要能继续打开。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;欢迎页保留&lt;/strong&gt;：但要改成”改了源码就能自动打包”的形式，不再手动 webpack。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;主站放国内，再留一个备用&lt;/strong&gt;：主站用阿里云 ESA（边缘安全加速）的 Pages，国内访问快；GitHub Pages 作为备用站点。&lt;/li&gt;
&lt;/ol&gt;&lt;p&gt;框架选 Astro。主题一开始用的是 &lt;a href=&quot;https://github.com/radishzzz/astro-theme-retypeset&quot; target=&quot;_blank&quot;&gt;Retypeset&lt;/a&gt;：排版干净，以文字为主。上线后觉得太素，文章列表也不能显示图片，又换成了 &lt;a href=&quot;https://github.com/CuteLeaf/Firefly&quot; target=&quot;_blank&quot;&gt;Firefly&lt;/a&gt;（基于 Fuwari 二次开发）：卡片式布局，有封面图、首页横幅、分类和侧边栏。换主题的过程见第十二节。&lt;/p&gt;&lt;p&gt;图片目录沿用 Typora 的习惯：每篇文章旁边一个同名的 &lt;code&gt;.assets&lt;/code&gt; 文件夹，文章里用相对路径引用。&lt;/p&gt;&lt;div&gt;&lt;figure&gt;&lt;figcaption&gt;&lt;/figcaption&gt;&lt;pre&gt;&lt;code&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;1&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;src/content/posts/git/&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;2&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;├── git常用命令.md&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;3&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;└── git常用命令.assets/&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;4&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;&lt;span&gt;    &lt;/span&gt;&lt;/span&gt;&lt;span&gt;└── 20180514152628079993927.png&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;/code&gt;&lt;/pre&gt;&lt;div&gt;&lt;div&gt;&lt;/div&gt;&lt;div&gt;&lt;/div&gt;&lt;/div&gt;&lt;/figure&gt;&lt;/div&gt;&lt;p&gt;这样做的好处是：Typora 粘贴图片时会自动放进这个文件夹；以后换 Obsidian 之类的编辑器，相对路径也照样能用；Astro 构建时会自动把这些图片压缩、转成 webp。&lt;/p&gt;&lt;/section&gt;
&lt;section&gt;&lt;h2&gt;三、从七牛找回图片&lt;a href=&quot;#三从七牛找回图片&quot;&gt;&lt;span&gt;#&lt;/span&gt;&lt;/a&gt;&lt;/h2&gt;&lt;p&gt;图片的域名过期了，但七牛的存储空间还在，文件都在里面，只是没有可用的域名能下载。&lt;/p&gt;&lt;section&gt;&lt;h3&gt;思路：绕过域名，直接用 API 下载&lt;a href=&quot;#思路绕过域名直接用-api-下载&quot;&gt;&lt;span&gt;#&lt;/span&gt;&lt;/a&gt;&lt;/h3&gt;&lt;p&gt;七牛提供 &lt;strong&gt;S3 兼容接口&lt;/strong&gt;，可以像操作 AWS S3 一样，用 AccessKey/SecretKey 签名请求，直接列出和下载空间里的文件，不需要绑定任何域名。&lt;/p&gt;&lt;p&gt;macOS 自带的 curl 8.x 内置了 AWS 签名功能：&lt;/p&gt;&lt;div&gt;&lt;figure&gt;&lt;figcaption&gt;&lt;span&gt;&lt;/span&gt;&lt;span&gt;Terminal window&lt;/span&gt;&lt;/figcaption&gt;&lt;pre&gt;&lt;code&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;1&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;curl&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;--aws-sigv4&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;&quot;aws:amz:cn-east-1:s3&quot;&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;\&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;2&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;     &lt;/span&gt;&lt;span&gt;--user&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;&quot;&lt;/span&gt;&lt;span&gt;$AK&lt;/span&gt;&lt;span&gt;:&lt;/span&gt;&lt;span&gt;$SK&lt;/span&gt;&lt;span&gt;&quot;&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;\&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;3&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;     &lt;/span&gt;&lt;span&gt;&quot;https://s3.cn-east-1.qiniucs.com/&amp;lt;空间名&amp;gt;/&amp;lt;文件名&amp;gt;&quot;&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;-o&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;文件名&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;/code&gt;&lt;/pre&gt;&lt;div&gt;&lt;div&gt;&lt;/div&gt;&lt;div&gt;&lt;/div&gt;&lt;/div&gt;&lt;/figure&gt;&lt;/div&gt;&lt;p&gt;写了一个脚本：先列出空间里所有文件，再逐个下载。AK/SK 运行时手动输入，不写进脚本，也不保存。&lt;/p&gt;&lt;/section&gt;&lt;section&gt;&lt;h3&gt;踩坑 1：签名一直报 &lt;code&gt;SignatureDoesNotMatch&lt;/code&gt;&lt;a href=&quot;#踩坑-1签名一直报-signaturedoesnotmatch&quot;&gt;&lt;span&gt;#&lt;/span&gt;&lt;/a&gt;&lt;/h3&gt;&lt;p&gt;所有区域都返回 403 签名错误。原因是&lt;strong&gt;控制台上看到的”空间名称”，不一定是 S3 接口里用的 bucket 名&lt;/strong&gt;。先调用 ListBuckets 接口列出真实的 bucket 名，再用它去请求，就好了。&lt;/p&gt;&lt;p&gt;&lt;strong&gt;原理&lt;/strong&gt;：AWS SigV4 签名会把请求方法、路径、主机名、时间、区域等信息一起做 HMAC 计算。路径里的 bucket 名写错，服务端算出来的签名就和你的对不上，它不会告诉你”bucket 不存在”，只会说签名不匹配。所以遇到签名错误，先怀疑参数，而不是密钥。&lt;/p&gt;&lt;/section&gt;&lt;section&gt;&lt;h3&gt;踩坑 2：&lt;code&gt;invalid args&lt;/code&gt;&lt;a href=&quot;#踩坑-2invalid-args&quot;&gt;&lt;span&gt;#&lt;/span&gt;&lt;/a&gt;&lt;/h3&gt;&lt;p&gt;改完后又报 &lt;code&gt;invalid args&lt;/code&gt;。打印出来一看，AK/SK 两个变量的长度都是 0：在一个终端里 &lt;code&gt;export&lt;/code&gt; 的变量，换了窗口就没了。改成脚本里用 &lt;code&gt;read&lt;/code&gt; 读取，就解决了。&lt;/p&gt;&lt;/section&gt;&lt;section&gt;&lt;h3&gt;踩坑 3：macOS 的 sed 不认 &lt;code&gt;\n&lt;/code&gt;&lt;a href=&quot;#踩坑-3macos-的-sed-不认-n&quot;&gt;&lt;span&gt;#&lt;/span&gt;&lt;/a&gt;&lt;/h3&gt;&lt;p&gt;解析 S3 返回的 XML 列表时，用 &lt;code&gt;sed&lt;/code&gt; 把标签替换成换行，在 Linux 上没问题，在 macOS 上却不生效。macOS 自带的是 BSD sed，替换内容里不支持 &lt;code&gt;\n&lt;/code&gt;、&lt;code&gt;\t&lt;/code&gt; 这种写法。改用 perl 解析，两边行为一致。&lt;/p&gt;&lt;/section&gt;&lt;section&gt;&lt;h3&gt;踩坑 4：中文文件名下载失败&lt;a href=&quot;#踩坑-4中文文件名下载失败&quot;&gt;&lt;span&gt;#&lt;/span&gt;&lt;/a&gt;&lt;/h3&gt;&lt;p&gt;178 个文件里有 6 个下载失败，全是中文文件名。macOS 自带的 bash 还是 3.2 版本，用它写的 URL 编码函数会按字符处理，不会按 UTF-8 的字节处理，中文就编码错了。签名用的路径和实际请求的路径对不上，又是签名错误。同样换成 perl 做 URL 编码，问题解决。&lt;/p&gt;&lt;p&gt;最后 178 个文件全部下载成功，文章实际用到的几十张图片一张不少。&lt;/p&gt;&lt;/section&gt;&lt;/section&gt;
&lt;section&gt;&lt;h2&gt;四、转换文章&lt;a href=&quot;#四转换文章&quot;&gt;&lt;span&gt;#&lt;/span&gt;&lt;/a&gt;&lt;/h2&gt;&lt;p&gt;写了一个一次性的 Python 脚本（&lt;code&gt;scripts/migrate-hexo.py&lt;/code&gt;），把 Hexo 文章转成 Astro 的格式。&lt;/p&gt;&lt;section&gt;&lt;h3&gt;文章开头的元信息（front matter）&lt;a href=&quot;#文章开头的元信息front-matter&quot;&gt;&lt;span&gt;#&lt;/span&gt;&lt;/a&gt;&lt;/h3&gt;&lt;p&gt;三套写法对比（Hexo 是原来的，Retypeset 是第一版，Firefly 是现在用的）：&lt;/p&gt;

&lt;table&gt;&lt;thead&gt;&lt;tr&gt;&lt;th&gt;Hexo&lt;/th&gt;&lt;th&gt;Retypeset&lt;/th&gt;&lt;th&gt;Firefly（现在）&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;title&lt;/code&gt;&lt;/td&gt;&lt;td&gt;&lt;code&gt;title&lt;/code&gt;&lt;/td&gt;&lt;td&gt;&lt;code&gt;title&lt;/code&gt;&lt;/td&gt;&lt;td&gt;不变&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;&lt;code&gt;date&lt;/code&gt;&lt;/td&gt;&lt;td&gt;&lt;code&gt;published&lt;/code&gt;&lt;/td&gt;&lt;td&gt;&lt;code&gt;published&lt;/code&gt;&lt;/td&gt;&lt;td&gt;统一加上 &lt;code&gt;+08:00&lt;/code&gt; 时区&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;无&lt;/td&gt;&lt;td&gt;&lt;code&gt;updated&lt;/code&gt;&lt;/td&gt;&lt;td&gt;&lt;code&gt;updated&lt;/code&gt;&lt;/td&gt;&lt;td&gt;更新时间，可选&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;&lt;code&gt;excerpt&lt;/code&gt;（里面嵌着 HTML 图片标签）&lt;/td&gt;&lt;td&gt;&lt;code&gt;description&lt;/code&gt;&lt;/td&gt;&lt;td&gt;&lt;code&gt;description&lt;/code&gt;&lt;/td&gt;&lt;td&gt;去掉 HTML，只留纯文字。不写的话主题会从正文截取&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;&lt;code&gt;tags&lt;/code&gt;&lt;/td&gt;&lt;td&gt;&lt;code&gt;tags&lt;/code&gt;&lt;/td&gt;&lt;td&gt;&lt;code&gt;tags&lt;/code&gt;&lt;/td&gt;&lt;td&gt;不变&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;&lt;code&gt;categories&lt;/code&gt;（可以多个）&lt;/td&gt;&lt;td&gt;没有&lt;/td&gt;&lt;td&gt;&lt;code&gt;category&lt;/code&gt;（只能一个）&lt;/td&gt;&lt;td&gt;Retypeset 没有分类；Firefly 有，按文章所在文件夹自动填上&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;image&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;&lt;code&gt;pin: 1&lt;/code&gt;&lt;/td&gt;&lt;td&gt;&lt;code&gt;pinned: true&lt;/code&gt;&lt;/td&gt;&lt;td&gt;置顶&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;&lt;code&gt;_draft&lt;/code&gt; 目录&lt;/td&gt;&lt;td&gt;&lt;code&gt;draft: true&lt;/code&gt;&lt;/td&gt;&lt;td&gt;&lt;code&gt;draft: true&lt;/code&gt;&lt;/td&gt;&lt;td&gt;草稿不会发布&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;&lt;code&gt;permalink&lt;/code&gt; 规则（全局）&lt;/td&gt;&lt;td&gt;&lt;code&gt;abbrlink&lt;/code&gt;&lt;/td&gt;&lt;td&gt;&lt;code&gt;slug&lt;/code&gt;&lt;/td&gt;&lt;td&gt;文章网址，比如 &lt;code&gt;git-commands&lt;/code&gt; 对应 &lt;code&gt;/posts/git-commands/&lt;/code&gt;。Retypeset 用的是自己的 &lt;code&gt;abbrlink&lt;/code&gt; 字段；换到 Firefly 后改用 Astro 自带的 &lt;code&gt;slug&lt;/code&gt;，见第十二节&lt;/td&gt;&lt;/tr&gt;&lt;/tbody&gt;&lt;/table&gt;&lt;p&gt;现在一篇文章开头的完整写法：&lt;/p&gt;&lt;div&gt;&lt;figure&gt;&lt;figcaption&gt;&lt;/figcaption&gt;&lt;pre&gt;&lt;code&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;1&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;---&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;2&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;title&lt;/span&gt;&lt;span&gt;: &lt;/span&gt;&lt;span&gt;git常用命令&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;3&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;published&lt;/span&gt;&lt;span&gt;: &lt;/span&gt;&lt;span&gt;2018-04-15T16:37:11+08:00&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;4&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;description&lt;/span&gt;&lt;span&gt;: &lt;/span&gt;&lt;span&gt;在开发过程中 git 已经是必不可少的版本控制工具……&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;5&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;tags&lt;/span&gt;&lt;span&gt;:&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;6&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;&lt;span&gt;  &lt;/span&gt;&lt;/span&gt;&lt;span&gt;- &lt;/span&gt;&lt;span&gt;git&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;7&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;category&lt;/span&gt;&lt;span&gt;: &lt;/span&gt;&lt;span&gt;Git&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;8&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;image&lt;/span&gt;&lt;span&gt;: &lt;/span&gt;&lt;span&gt;./git常用命令.assets/20180514152628079993927.png&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;9&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;draft&lt;/span&gt;&lt;span&gt;: &lt;/span&gt;&lt;span&gt;false&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;10&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;slug&lt;/span&gt;&lt;span&gt;: &lt;/span&gt;&lt;span&gt;git-commands&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;11&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;---&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;/code&gt;&lt;/pre&gt;&lt;div&gt;&lt;div&gt;&lt;/div&gt;&lt;div&gt;&lt;/div&gt;&lt;/div&gt;&lt;/figure&gt;&lt;/div&gt;&lt;p&gt;必填的只有 &lt;code&gt;title&lt;/code&gt; 和 &lt;code&gt;published&lt;/code&gt;，其他都可以省略。&lt;/p&gt;&lt;/section&gt;&lt;section&gt;&lt;h3&gt;封面怎么写&lt;a href=&quot;#封面怎么写&quot;&gt;&lt;span&gt;#&lt;/span&gt;&lt;/a&gt;&lt;/h3&gt;&lt;p&gt;封面要在 &lt;code&gt;image&lt;/code&gt; 里手动指定，主题不会自动拿正文里的图：&lt;/p&gt;&lt;div&gt;&lt;figure&gt;&lt;figcaption&gt;&lt;/figcaption&gt;&lt;pre&gt;&lt;code&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;1&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;image&lt;/span&gt;&lt;span&gt;: &lt;/span&gt;&lt;span&gt;./文章名.assets/cover.png&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;/code&gt;&lt;/pre&gt;&lt;div&gt;&lt;div&gt;&lt;/div&gt;&lt;div&gt;&lt;/div&gt;&lt;/div&gt;&lt;/figure&gt;&lt;/div&gt;&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;路径&lt;/strong&gt;：相对这篇 &lt;code&gt;.md&lt;/code&gt; 文件，和正文里插图的写法一样。用 Typora 粘贴图片后，把正文里那张图的路径复制过来就行；也可以专门放一张不在正文里的图。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;不填或者留空&lt;/strong&gt;：这篇没有封面，列表里只显示文字。&lt;code&gt;pnpm new-post&lt;/code&gt; 生成的模板默认是 &lt;code&gt;image: &apos;&apos;&lt;/code&gt;。主题有”随机封面”功能（&lt;code&gt;image: &quot;api&quot;&lt;/code&gt;，从网上的随机图接口取图），没开。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;会出现在哪&lt;/strong&gt;：文章列表的卡片配图、分享海报、搜索引擎和社交软件的预览图（&lt;code&gt;og:image&lt;/code&gt;）。文章详情页&lt;strong&gt;不显示&lt;/strong&gt;。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;为什么详情页不显示&lt;/strong&gt;：Firefly 默认会在文章标题下面再放一张大封面（&lt;code&gt;src/config/coverImageConfig.ts&lt;/code&gt; 里的 &lt;code&gt;enableInPost&lt;/code&gt;）。迁移时脚本给 11 篇有图的文章填的封面都是正文第一张图，开着这个选项，进文章就会先看到封面、紧接着又是同一张图。所以把 &lt;code&gt;enableInPost&lt;/code&gt; 改成了 &lt;code&gt;false&lt;/code&gt;。这是主题给用户改的配置项，不用动源码，也不用删文章里的图。以后如果想让某篇文章顶部有大图，就直接把图放在正文开头。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;后来决定全部不用封面&lt;/strong&gt;（2026-10-31）：试过给两篇新文章画专门的封面插画，但主题的封面框是固定比例的（电脑端列表约 1.5 : 1，手机端网格 2 : 1），图片按填满方式显示，不同比例下总要裁掉一部分，画面里的文字很容易被切掉。最后把所有文章的 &lt;code&gt;image&lt;/code&gt; 都删了（10 篇，封面原本都是正文里的第一张图，正文里的图都还在），列表只显示文字；想要配图的文章，把图放在正文开头。&lt;code&gt;pnpm new-post&lt;/code&gt; 模板里的 &lt;code&gt;image: &apos;&apos;&lt;/code&gt; 保留，以后哪篇确实想要封面，再手动填上。代价是分享海报和社交软件的预览里也就没有图了。&lt;/li&gt;
&lt;/ul&gt;&lt;p&gt;有个小坑：Hexo 的 &lt;code&gt;excerpt&lt;/code&gt; 里直接写了没加引号的 HTML，YAML 解析会报错。脚本遇到解析失败时，先去掉 &lt;code&gt;excerpt&lt;/code&gt; 再重试。&lt;/p&gt;&lt;/section&gt;&lt;section&gt;&lt;h3&gt;正文里的图片&lt;a href=&quot;#正文里的图片&quot;&gt;&lt;span&gt;#&lt;/span&gt;&lt;/a&gt;&lt;/h3&gt;&lt;ol&gt;
&lt;li&gt;正文里的 &lt;code&gt;&amp;lt;img src=&quot;...&quot;&amp;gt;&lt;/code&gt; 统一转成 Markdown 的 &lt;code&gt;![](...)&lt;/code&gt; 写法，Astro 才会处理这些图片（压缩、生成多种尺寸）。&lt;/li&gt;
&lt;li&gt;所有指向旧图床的地址，改成 &lt;code&gt;./文章名.assets/文件名&lt;/code&gt;，同时把图片复制到对应文件夹。&lt;/li&gt;
&lt;li&gt;转换完后检查，确认没有残留的旧域名。&lt;/li&gt;
&lt;/ol&gt;&lt;/section&gt;&lt;section&gt;&lt;h3&gt;文件命名&lt;a href=&quot;#文件命名&quot;&gt;&lt;span&gt;#&lt;/span&gt;&lt;/a&gt;&lt;/h3&gt;&lt;p&gt;最开始文件名用的是英文（和网址一致），后来改成了文章标题，比如 &lt;code&gt;git常用命令.md&lt;/code&gt;。因为&lt;strong&gt;网址由开头的 &lt;code&gt;slug&lt;/code&gt; 决定，和文件名、文件夹都无关&lt;/strong&gt;（迁移时叫 &lt;code&gt;abbrlink&lt;/code&gt;，换主题后改成了 Astro 自带的 &lt;code&gt;slug&lt;/code&gt;），文件名用中文在编辑器里更好认，移动文件或改名也不会影响网址。文件夹按主题分，统一用英文。&lt;/p&gt;&lt;p&gt;文件名里要避开两类字符：&lt;/p&gt;&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;空格&lt;/strong&gt;：Markdown 图片链接里的路径不能有空格，有空格就得用 &lt;code&gt;&amp;lt;&amp;gt;&lt;/code&gt; 包起来，很容易出错。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;&lt;code&gt;+&lt;/code&gt;&lt;/strong&gt;：在 URL 的查询参数里，&lt;code&gt;+&lt;/code&gt; 会被当成空格解码。开发模式下图片地址正好是通过查询参数传递的。&lt;/li&gt;
&lt;/ul&gt;&lt;/section&gt;&lt;/section&gt;
&lt;section&gt;&lt;h2&gt;五、旧链接怎么不失效&lt;a href=&quot;#五旧链接怎么不失效&quot;&gt;&lt;span&gt;#&lt;/span&gt;&lt;/a&gt;&lt;/h2&gt;&lt;p&gt;Hexo 的网址格式是 &lt;code&gt;/年/月/日/分类/标题/&lt;/code&gt;，比如 &lt;code&gt;/2018/04/15/git/git常用命令/&lt;/code&gt;。新博客是 &lt;code&gt;/posts/git-commands/&lt;/code&gt;。&lt;/p&gt;&lt;p&gt;静态托管没有服务器可以配置重定向规则（至少不能每个平台都配），所以用了最通用的办法：&lt;strong&gt;在旧地址生成一个只做跳转的页面&lt;/strong&gt;。&lt;/p&gt;&lt;div&gt;&lt;figure&gt;&lt;figcaption&gt;&lt;/figcaption&gt;&lt;pre&gt;&lt;code&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;1&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;&amp;lt;!&lt;/span&gt;&lt;span&gt;doctype&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;html&lt;/span&gt;&lt;span&gt;&amp;gt;&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;2&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;&amp;lt;&lt;/span&gt;&lt;span&gt;html&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;lang&lt;/span&gt;&lt;span&gt;=&lt;/span&gt;&lt;span&gt;&quot;zh&quot;&lt;/span&gt;&lt;span&gt;&amp;gt;&amp;lt;&lt;/span&gt;&lt;span&gt;head&lt;/span&gt;&lt;span&gt;&amp;gt;&amp;lt;&lt;/span&gt;&lt;span&gt;meta&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;charset&lt;/span&gt;&lt;span&gt;=&lt;/span&gt;&lt;span&gt;&quot;utf-8&quot;&lt;/span&gt;&lt;span&gt;&amp;gt;&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;3&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;&amp;lt;&lt;/span&gt;&lt;span&gt;title&lt;/span&gt;&lt;span&gt;&amp;gt;git常用命令&amp;lt;/&lt;/span&gt;&lt;span&gt;title&lt;/span&gt;&lt;span&gt;&amp;gt;&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;4&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;&amp;lt;&lt;/span&gt;&lt;span&gt;link&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;rel&lt;/span&gt;&lt;span&gt;=&lt;/span&gt;&lt;span&gt;&quot;canonical&quot;&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;href&lt;/span&gt;&lt;span&gt;=&lt;/span&gt;&lt;span&gt;&quot;/posts/git-commands/&quot;&lt;/span&gt;&lt;span&gt;&amp;gt;&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;5&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;&amp;lt;&lt;/span&gt;&lt;span&gt;meta&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;name&lt;/span&gt;&lt;span&gt;=&lt;/span&gt;&lt;span&gt;&quot;robots&quot;&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;content&lt;/span&gt;&lt;span&gt;=&lt;/span&gt;&lt;span&gt;&quot;noindex&quot;&lt;/span&gt;&lt;span&gt;&amp;gt;&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;6&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;&amp;lt;&lt;/span&gt;&lt;span&gt;meta&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;http-equiv&lt;/span&gt;&lt;span&gt;=&lt;/span&gt;&lt;span&gt;&quot;refresh&quot;&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;content&lt;/span&gt;&lt;span&gt;=&lt;/span&gt;&lt;span&gt;&quot;0; url=/posts/git-commands/&quot;&lt;/span&gt;&lt;span&gt;&amp;gt;&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;7&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;&amp;lt;/&lt;/span&gt;&lt;span&gt;head&lt;/span&gt;&lt;span&gt;&amp;gt;&amp;lt;&lt;/span&gt;&lt;span&gt;body&lt;/span&gt;&lt;span&gt;&amp;gt;&amp;lt;&lt;/span&gt;&lt;span&gt;p&lt;/span&gt;&lt;span&gt;&amp;gt;文章已搬到新地址：&amp;lt;&lt;/span&gt;&lt;span&gt;a&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;href&lt;/span&gt;&lt;span&gt;=&lt;/span&gt;&lt;span&gt;&quot;/posts/git-commands/&quot;&lt;/span&gt;&lt;span&gt;&amp;gt;git常用命令&amp;lt;/&lt;/span&gt;&lt;span&gt;a&lt;/span&gt;&lt;span&gt;&amp;gt;&amp;lt;/&lt;/span&gt;&lt;span&gt;p&lt;/span&gt;&lt;span&gt;&amp;gt;&amp;lt;/&lt;/span&gt;&lt;span&gt;body&lt;/span&gt;&lt;span&gt;&amp;gt;&amp;lt;/&lt;/span&gt;&lt;span&gt;html&lt;/span&gt;&lt;span&gt;&amp;gt;&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;/code&gt;&lt;/pre&gt;&lt;div&gt;&lt;div&gt;&lt;/div&gt;&lt;div&gt;&lt;/div&gt;&lt;/div&gt;&lt;/figure&gt;&lt;/div&gt;&lt;p&gt;这些页面放在 Astro 的 &lt;code&gt;public/&lt;/code&gt; 目录里，构建时原样复制。&lt;/p&gt;&lt;ul&gt;
&lt;li&gt;&lt;code&gt;meta refresh&lt;/code&gt; 让浏览器立即跳转。&lt;/li&gt;
&lt;li&gt;&lt;code&gt;canonical&lt;/code&gt; 告诉搜索引擎，正式地址是新的那个。&lt;/li&gt;
&lt;li&gt;&lt;code&gt;noindex&lt;/code&gt; 让搜索引擎不要收录这个跳转页。&lt;/li&gt;
&lt;/ul&gt;&lt;p&gt;这个方案和主题、部署平台都无关，换主题、换平台都不用改。&lt;/p&gt;&lt;p&gt;后来（2026-10-25）整理旧文章时，把一批内容单薄的文章设成了草稿，不再发布。它们的旧地址如果还指向 &lt;code&gt;/posts/...&lt;/code&gt;，就会跳到一个不存在的页面，构建检查也会报错。处理方式是：内容被合并到其他文章的，跳转改指向新文章（比如 webbench 压测那篇合并进了 Nginx 那篇）；单纯下线的，跳转改指向归档页 &lt;code&gt;/archive/&lt;/code&gt;，页面上说明这篇旧文已下线。&lt;/p&gt;&lt;/section&gt;
&lt;section&gt;&lt;h2&gt;六、欢迎页：从手动打包到 Astro 自动打包&lt;a href=&quot;#六欢迎页从手动打包到-astro-自动打包&quot;&gt;&lt;span&gt;#&lt;/span&gt;&lt;/a&gt;&lt;/h2&gt;&lt;p&gt;原来的欢迎页由几部分组成：一个 316KB 的 HTML，里面塞满了内联 SVG；动画用 GSAP 1.x 的 TimelineMax；还依赖 jQuery、jquery-ui 的滑块、drawsvg 描边插件、randomColor 随机颜色。当年用 webpack 手动打包，每次改都要重新打包一遍。&lt;/p&gt;&lt;section&gt;&lt;h3&gt;改写方式&lt;a href=&quot;#改写方式&quot;&gt;&lt;span&gt;#&lt;/span&gt;&lt;/a&gt;&lt;/h3&gt;&lt;ul&gt;
&lt;li&gt;新建一个 &lt;code&gt;src/pages/welcome.astro&lt;/code&gt;，放在 &lt;code&gt;/welcome/&lt;/code&gt;。最初是不套博客主题布局的独立页面，后来为了让音乐不中断，套进了主题的页面框架（见第十二节）。&lt;/li&gt;
&lt;li&gt;大幅 SVG 插画拆成组件，放在 &lt;code&gt;src/components/welcome/&lt;/code&gt;。&lt;/li&gt;
&lt;li&gt;动画逻辑写成 TypeScript，放在页面的 &lt;code&gt;&amp;lt;script&amp;gt;&lt;/code&gt; 里。&lt;strong&gt;Astro（底层是 Vite）会自动打包压缩&lt;/strong&gt;，以后改完源码直接 &lt;code&gt;pnpm build&lt;/code&gt; 就行。&lt;/li&gt;
&lt;/ul&gt;&lt;/section&gt;&lt;section&gt;&lt;h3&gt;为什么没用 GSAP&lt;a href=&quot;#为什么没用-gsap&quot;&gt;&lt;span&gt;#&lt;/span&gt;&lt;/a&gt;&lt;/h3&gt;&lt;p&gt;当时我的开发环境装不了 npm 依赖，就算装上了，也会和项目锁定的依赖版本对不上。所以干脆自己写了一个两百多行的小补间库（&lt;code&gt;tween.ts&lt;/code&gt;），刚好够用：&lt;/p&gt;&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;时间线采用”确定性渲染”&lt;/strong&gt;：给定任意时间点 t，从初始状态出发，按开始时间依次计算每个补间在 t 时刻的值。这样拖动进度条到任意位置都能正确显示，循环播放也只是 &lt;code&gt;t % 总时长&lt;/code&gt;。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;每个补间的起始值预先算好&lt;/strong&gt;：等于它开始那一刻，前面所有补间叠加出来的值，相当于 GSAP 在补间第一次渲染时才记录起始值的行为。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;SVG 的旋转和缩放以元素中心为原点&lt;/strong&gt;：用 &lt;code&gt;getBBox()&lt;/code&gt; 求出中心，拼成 &lt;code&gt;translate(x y) rotate(r ox oy) translate(ox oy) scale(s) translate(-ox -oy)&lt;/code&gt;，等同于 GSAP 的 &lt;code&gt;transformOrigin: &quot;50% 50%&quot;&lt;/code&gt;。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;描边动画&lt;/strong&gt;：用 &lt;code&gt;stroke-dasharray&lt;/code&gt; 从 &lt;code&gt;0 L&lt;/code&gt; 过渡到 &lt;code&gt;L 0&lt;/code&gt;（L 是路径总长）。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;缓动函数&lt;/strong&gt;：照着 GSAP 的公式实现，比如 Power3.easeInOut 就是四次方的缓入缓出，Back.easeInOut 带回弹。&lt;/li&gt;
&lt;li&gt;jquery-ui 的滑块换成原生的 &lt;code&gt;&amp;lt;input type=&quot;range&quot;&amp;gt;&lt;/code&gt;，背景圆点的颜色变化直接用 CSS transition。&lt;/li&gt;
&lt;li&gt;菜单里的音乐按钮，后来从”跳到网易云主页”一路改成了”全站共用一个播放器、切换页面音乐不断”，过程见第十二节的”音乐”一段。&lt;/li&gt;
&lt;/ul&gt;&lt;/section&gt;&lt;section&gt;&lt;h3&gt;用截图对比来保证还原度&lt;a href=&quot;#用截图对比来保证还原度&quot;&gt;&lt;span&gt;#&lt;/span&gt;&lt;/a&gt;&lt;/h3&gt;&lt;p&gt;为了确认效果和原来一样，用 Playwright 打开新旧两个页面，用它的虚拟时钟把动画快进到同样的时间点，然后截图对比。这个过程中发现了几个原版里”看起来是这样，实际不是”的细节：&lt;/p&gt;&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;花瓶插画其实没有描边动画&lt;/strong&gt;。原代码对它调用了描边函数，但参数里有个 &lt;code&gt;fill: undefined&lt;/code&gt;，GSAP 处理时出错，那个补间实际没生效。花瓶的线条其实是填充出来的形状，看到的变色是填充色在变。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;右侧中心图案的变色看不出来&lt;/strong&gt;。原代码改的是外层 &lt;code&gt;&amp;lt;g&amp;gt;&lt;/code&gt; 的 fill，但里面每条 path 自己都设了 fill 属性，会覆盖从父元素继承来的颜色。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;背景圆点一直是半透明的&lt;/strong&gt;。原代码同时写了 &lt;code&gt;opacity: 1&lt;/code&gt; 和 &lt;code&gt;css: {...}&lt;/code&gt;。在 GSAP 1.x 里，一旦提供了 &lt;code&gt;css&lt;/code&gt; 对象，外层的属性就不再被当作 CSS 处理，所以 &lt;code&gt;opacity: 1&lt;/code&gt; 根本没生效，圆点一直保持 CSS 里的 0.22。&lt;/li&gt;
&lt;/ul&gt;&lt;p&gt;这些”bug”都按原样还原了，因为最终要的是看起来一样。&lt;/p&gt;&lt;/section&gt;&lt;/section&gt;
&lt;section&gt;&lt;h2&gt;七、本地环境的小插曲&lt;a href=&quot;#七本地环境的小插曲&quot;&gt;&lt;span&gt;#&lt;/span&gt;&lt;/a&gt;&lt;/h2&gt;&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;不是管理员账号&lt;/strong&gt;：这台 Mac 没有管理员权限，Homebrew 找不到现成的安装包，只能从源码编译 Node，结果连 LLVM 一起编译，几个小时都编译不完。改用 nvm 安装 Node：nvm 下载官方编译好的版本，装在用户目录下，不需要管理员权限，几分钟就好。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;私有仓库 clone 要登录&lt;/strong&gt;：这和 git config 没关系，git config 里的用户名和邮箱只用来给提交署名。在 GitHub 生成一个只授权这个仓库的细粒度访问令牌（Fine-grained token），clone 时当作密码输入，macOS 钥匙串会记住它。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;&lt;code&gt;Command &quot;dev&quot; not found&lt;/code&gt;&lt;/strong&gt;：在错误的目录里运行了 &lt;code&gt;pnpm dev&lt;/code&gt;。那个目录里没有项目的 &lt;code&gt;package.json&lt;/code&gt;，pnpm 当然找不到 &lt;code&gt;dev&lt;/code&gt; 命令。&lt;/li&gt;
&lt;/ul&gt;&lt;/section&gt;
&lt;section&gt;&lt;h2&gt;八、开发模式和构建模式不一样&lt;a href=&quot;#八开发模式和构建模式不一样&quot;&gt;&lt;span&gt;#&lt;/span&gt;&lt;/a&gt;&lt;/h2&gt;&lt;p&gt;本地运行 &lt;code&gt;pnpm dev&lt;/code&gt; 时，发现图片有时显示不出来，页面样式也乱了。但运行 &lt;code&gt;pnpm build &amp;amp;&amp;amp; pnpm preview&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;code&gt;pnpm dev&lt;/code&gt;&lt;/th&gt;&lt;th&gt;&lt;code&gt;pnpm build&lt;/code&gt; + &lt;code&gt;pnpm preview&lt;/code&gt;&lt;/th&gt;&lt;/tr&gt;&lt;/thead&gt;&lt;tbody&gt;&lt;tr&gt;&lt;td&gt;页面&lt;/td&gt;&lt;td&gt;打开时才生成&lt;/td&gt;&lt;td&gt;一次性生成到 &lt;code&gt;dist/&lt;/code&gt;&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;图片&lt;/td&gt;&lt;td&gt;第一次请求时才转 webp，大图要等几秒&lt;/td&gt;&lt;td&gt;构建时全部转好&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;样式&lt;/td&gt;&lt;td&gt;边扫描边生成（Retypeset 用的 UnoCSS），偶尔会缺&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;显示&lt;/strong&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;/tbody&gt;&lt;/table&gt;&lt;p&gt;图片还带着 &lt;code&gt;loading=&quot;lazy&quot;&lt;/code&gt;，滚到附近才开始加载。开发模式下，懒加载再加上第一次现场转码，看起来就像图片打不开。遇到这种情况刷新一下就好，线上不会这样。&lt;/p&gt;&lt;/section&gt;
&lt;section&gt;&lt;h2&gt;九、自动检查&lt;a href=&quot;#九自动检查&quot;&gt;&lt;span&gt;#&lt;/span&gt;&lt;/a&gt;&lt;/h2&gt;&lt;p&gt;光看构建成功还不够：图片少了一张、旧链接跳转到不存在的页面、草稿被误发布……这些都不会让构建失败。所以写了 &lt;code&gt;scripts/verify-build.sh&lt;/code&gt;，每次构建后自动检查：&lt;/p&gt;&lt;ul&gt;
&lt;li&gt;每篇已发布的文章都生成了页面；&lt;/li&gt;
&lt;li&gt;每篇文章页面里的图片数量，不少于 Markdown 里引用的数量；&lt;/li&gt;
&lt;li&gt;页面里不再出现过期的旧图床域名；&lt;/li&gt;
&lt;li&gt;草稿没有被发布；&lt;/li&gt;
&lt;li&gt;每个旧链接跳转页都在，并且跳转目标存在；&lt;/li&gt;
&lt;li&gt;欢迎页存在，菜单链接和打包后的脚本都在。&lt;/li&gt;
&lt;/ul&gt;&lt;p&gt;检查脚本先用”故意弄坏”的情况测试过，确认它真的能发现问题。然后接入 GitHub Actions：每次推送都执行一遍构建和检查。&lt;/p&gt;&lt;p&gt;一个小细节：检查欢迎页有没有 &lt;code&gt;&amp;lt;script type=&quot;module&quot;&amp;gt;&lt;/code&gt; 时，第一次检查失败了。原因是 HTML 压缩工具会&lt;strong&gt;调整属性顺序、去掉属性的引号&lt;/strong&gt;，构建出来的实际是 &lt;code&gt;&amp;lt;script src=... type=module&amp;gt;&lt;/code&gt;。所以检查规则要写得和属性顺序、引号都无关。&lt;/p&gt;&lt;/section&gt;
&lt;section&gt;&lt;h2&gt;十、分支整理&lt;a href=&quot;#十分支整理&quot;&gt;&lt;span&gt;#&lt;/span&gt;&lt;/a&gt;&lt;/h2&gt;&lt;ul&gt;
&lt;li&gt;原来的 &lt;code&gt;master&lt;/code&gt; 是 Hexo 生成的旧网站，改名为 &lt;code&gt;hexo-site&lt;/code&gt; 留作存档。&lt;/li&gt;
&lt;li&gt;新的 Astro 项目从 &lt;code&gt;astro&lt;/code&gt; 分支改名为 &lt;code&gt;master&lt;/code&gt;，作为默认分支。&lt;/li&gt;
&lt;/ul&gt;&lt;p&gt;默认分支改了以后，GitHub 的 Dependabot 安全提醒（之前报的都是旧 Hexo 的依赖）也随之消失了，因为它只检查默认分支。&lt;/p&gt;&lt;/section&gt;
&lt;section&gt;&lt;h2&gt;十一、部署&lt;a href=&quot;#十一部署&quot;&gt;&lt;span&gt;#&lt;/span&gt;&lt;/a&gt;&lt;/h2&gt;&lt;section&gt;&lt;h3&gt;GitHub Pages（备用站）&lt;a href=&quot;#github-pages备用站&quot;&gt;&lt;span&gt;#&lt;/span&gt;&lt;/a&gt;&lt;/h3&gt;&lt;p&gt;&lt;strong&gt;私有仓库用 GitHub Pages 要付费&lt;/strong&gt;（GitHub Pro）。博客内容本来就是公开的，所以直接把仓库改成了公开。公开前确认过：仓库里没有密钥，草稿被别人看到也无所谓。&lt;/p&gt;&lt;p&gt;接着又碰到一个报错：GitHub 自带的 Jekyll 构建失败，报 &lt;code&gt;Invalid YAML front matter in .../NoteIcon.astro&lt;/code&gt;。&lt;/p&gt;&lt;p&gt;&lt;strong&gt;原因&lt;/strong&gt;：Pages 的来源还是”Deploy from a branch”，GitHub 会把分支内容当成 Jekyll 网站来构建，它把 &lt;code&gt;.astro&lt;/code&gt; 文件开头的 &lt;code&gt;---&lt;/code&gt; 当成了 YAML 元信息。&lt;strong&gt;解决&lt;/strong&gt;：把来源改成”GitHub Actions”，由我们自己的工作流构建 Astro，再用 &lt;code&gt;actions/deploy-pages&lt;/code&gt; 发布。&lt;/p&gt;&lt;p&gt;Custom domain 留空，用默认地址 &lt;code&gt;tracerzzz.github.io&lt;/code&gt;。因为一个域名同一时间只能解析到一个地方，&lt;code&gt;tracerzzz.cn&lt;/code&gt; 留给主站 ESA。&lt;/p&gt;&lt;/section&gt;&lt;section&gt;&lt;h3&gt;阿里云 ESA（主站）&lt;a href=&quot;#阿里云-esa主站&quot;&gt;&lt;span&gt;#&lt;/span&gt;&lt;/a&gt;&lt;/h3&gt;&lt;section&gt;&lt;h4&gt;三方的关系&lt;a href=&quot;#三方的关系&quot;&gt;&lt;span&gt;#&lt;/span&gt;&lt;/a&gt;&lt;/h4&gt;&lt;div&gt;&lt;figure&gt;&lt;figcaption&gt;&lt;/figcaption&gt;&lt;pre&gt;&lt;code&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;1&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;访客输入 tracerzzz.cn&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;2&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;&lt;span&gt;  &lt;/span&gt;&lt;/span&gt;&lt;span&gt;│&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;3&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;&lt;span&gt;  &lt;/span&gt;&lt;/span&gt;&lt;span&gt;▼&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;4&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;① 域名注册商（万网）：登记&quot;这个域名由谁来解析&quot;&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;5&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;&lt;span&gt;  &lt;/span&gt;&lt;/span&gt;&lt;span&gt;│&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;6&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;&lt;span&gt;  &lt;/span&gt;&lt;/span&gt;&lt;span&gt;▼&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;7&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;② DNS 解析：把域名翻译成服务器地址&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;8&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;&lt;span&gt;  &lt;/span&gt;&lt;/span&gt;&lt;span&gt;│&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;9&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;&lt;span&gt;  &lt;/span&gt;&lt;/span&gt;&lt;span&gt;▼&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;10&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;③ ESA：边缘节点 + Pages 托管网站&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;/code&gt;&lt;/pre&gt;&lt;div&gt;&lt;div&gt;&lt;/div&gt;&lt;div&gt;&lt;/div&gt;&lt;/div&gt;&lt;/figure&gt;&lt;/div&gt;&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;ESA 站点&lt;/strong&gt;是 &lt;code&gt;tracerzzz.cn&lt;/code&gt; 在 ESA 里的总管理处：DNS 解析、HTTPS 证书、缓存、安全防护、跳转规则都在这里。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;ESA Pages 项目&lt;/strong&gt;是网站本身：从 GitHub 拉代码，构建，发布。&lt;/li&gt;
&lt;li&gt;关系是：&lt;strong&gt;Pages 产出网站，站点把网站挂到域名上&lt;/strong&gt;。&lt;/li&gt;
&lt;/ul&gt;&lt;/section&gt;&lt;section&gt;&lt;h4&gt;NS 接入&lt;a href=&quot;#ns-接入&quot;&gt;&lt;span&gt;#&lt;/span&gt;&lt;/a&gt;&lt;/h4&gt;&lt;p&gt;接入方式有两种：&lt;/p&gt;&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;CNAME 接入&lt;/strong&gt;：DNS 留在原来的服务商，每个子域名手动加一条 CNAME 指向 ESA。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;NS 接入&lt;/strong&gt;：把域名的 DNS 服务器整体交给 ESA。&lt;/li&gt;
&lt;/ul&gt;&lt;p&gt;选了 NS 接入：在万网把 &lt;code&gt;tracerzzz.cn&lt;/code&gt; 的 DNS 服务器改成 ESA 给的两个地址，以后所有解析都在 ESA 里管理。根域名也能直接用，Pages 绑定域名时会自动生成解析记录。&lt;/p&gt;&lt;p&gt;&lt;strong&gt;改完 NS，本机 dig 查到的还是旧的&lt;/strong&gt;。分层排查：&lt;/p&gt;&lt;div&gt;&lt;figure&gt;&lt;figcaption&gt;&lt;span&gt;&lt;/span&gt;&lt;span&gt;Terminal window&lt;/span&gt;&lt;/figcaption&gt;&lt;pre&gt;&lt;code&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;1&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;# 直接问 .cn 注册局，不走任何缓存&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;2&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;dig&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;NS&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;tracerzzz.cn&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;@a.dns.cn&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;+norec&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;/code&gt;&lt;/pre&gt;&lt;div&gt;&lt;div&gt;&lt;/div&gt;&lt;div&gt;&lt;/div&gt;&lt;/div&gt;&lt;/figure&gt;&lt;/div&gt;&lt;p&gt;注册局返回的已经是 ESA 的 NS（&lt;code&gt;*.ns.atrustdns.com&lt;/code&gt;），TTL 是 86400 秒。也就是说修改已经生效，只是各级 DNS 的缓存最长要 24 小时才会过期。ESA 验证时查的是权威结果，所以不用等缓存，可以直接验证通过。&lt;/p&gt;&lt;/section&gt;&lt;section&gt;&lt;h4&gt;Pages 构建失败两次&lt;a href=&quot;#pages-构建失败两次&quot;&gt;&lt;span&gt;#&lt;/span&gt;&lt;/a&gt;&lt;/h4&gt;&lt;ol&gt;
&lt;li&gt;&lt;strong&gt;&lt;code&gt;Unknown command: &quot;build&quot;&lt;/code&gt;&lt;/strong&gt;：控制台里填了 &lt;code&gt;npm build&lt;/code&gt;，npm 没有这个命令，应该是 &lt;code&gt;npm run build&lt;/code&gt;。而且项目用的是 pnpm，安装和构建都应该用 pnpm。ESA 的构建环境里没有预装 pnpm，所以安装命令要写成 &lt;code&gt;npm install -g pnpm &amp;amp;&amp;amp; pnpm install&lt;/code&gt;：先用 npm 把 pnpm 装上，再用 pnpm 按锁文件安装依赖。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;&lt;code&gt;Both assets and function js file are not found&lt;/code&gt;&lt;/strong&gt;：构建其实成功了，但没告诉 ESA 发布哪个目录。&lt;/li&gt;
&lt;/ol&gt;&lt;p&gt;为了不再依赖控制台设置，在仓库根目录加了 &lt;code&gt;esa.jsonc&lt;/code&gt;：&lt;/p&gt;&lt;div&gt;&lt;figure&gt;&lt;figcaption&gt;&lt;/figcaption&gt;&lt;pre&gt;&lt;code&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;1&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;{&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;2&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;  &lt;/span&gt;&lt;span&gt;&quot;installCommand&quot;&lt;/span&gt;&lt;span&gt;: &lt;/span&gt;&lt;span&gt;&quot;npm install -g pnpm &amp;amp;&amp;amp; pnpm install&quot;&lt;/span&gt;&lt;span&gt;,&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;3&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;  &lt;/span&gt;&lt;span&gt;&quot;buildCommand&quot;&lt;/span&gt;&lt;span&gt;: &lt;/span&gt;&lt;span&gt;&quot;pnpm build&quot;&lt;/span&gt;&lt;span&gt;,&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;4&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;  &lt;/span&gt;&lt;span&gt;&quot;assets&quot;&lt;/span&gt;&lt;span&gt;: {&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;5&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;    &lt;/span&gt;&lt;span&gt;&quot;directory&quot;&lt;/span&gt;&lt;span&gt;: &lt;/span&gt;&lt;span&gt;&quot;./dist&quot;&lt;/span&gt;&lt;span&gt;,&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;6&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;    &lt;/span&gt;&lt;span&gt;&quot;notFoundStrategy&quot;&lt;/span&gt;&lt;span&gt;: &lt;/span&gt;&lt;span&gt;&quot;404Page&quot;&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;7&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;&lt;span&gt;  &lt;/span&gt;&lt;/span&gt;&lt;span&gt;}&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;8&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;}&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;/code&gt;&lt;/pre&gt;&lt;div&gt;&lt;div&gt;&lt;/div&gt;&lt;div&gt;&lt;/div&gt;&lt;/div&gt;&lt;/figure&gt;&lt;/div&gt;&lt;p&gt;有了这个文件，ESA 会优先用它，控制台的设置就不起作用了。配置跟着代码走，修改也有记录可查。&lt;/p&gt;&lt;/section&gt;&lt;section&gt;&lt;h4&gt;绑定域名：为什么 DNS 记录列表是空的&lt;a href=&quot;#绑定域名为什么-dns-记录列表是空的&quot;&gt;&lt;span&gt;#&lt;/span&gt;&lt;/a&gt;&lt;/h4&gt;&lt;p&gt;在 Pages 项目里绑定 &lt;code&gt;tracerzzz.cn&lt;/code&gt; 后，站点的 DNS 记录列表里却显示 0 条。原来 Pages 绑定的域名不会显示成普通的 A 或 CNAME 记录，而是单独列在”边缘函数记录”里。&lt;/p&gt;&lt;p&gt;背后的原理：&lt;/p&gt;&lt;ol&gt;
&lt;li&gt;NS 已经交给 ESA，所以任何人查询 &lt;code&gt;tracerzzz.cn&lt;/code&gt;，最终都会问到 ESA 的 DNS。&lt;/li&gt;
&lt;li&gt;ESA 的 DNS 发现这个域名绑定了 Pages，就&lt;strong&gt;返回离访客最近的边缘节点 IP&lt;/strong&gt;。&lt;/li&gt;
&lt;li&gt;浏览器连到这个节点，请求里带着 &lt;code&gt;Host: tracerzzz.cn&lt;/code&gt;。节点根据它找到对应的 Pages 项目，再去找当前部署版本里的文件。比如请求 &lt;code&gt;/posts/git-commands/&lt;/code&gt;，就返回 &lt;code&gt;dist/posts/git-commands/index.html&lt;/code&gt;。&lt;/li&gt;
&lt;li&gt;文件第一次被请求时从 ESA 的存储读取，之后缓存在节点上。&lt;/li&gt;
&lt;/ol&gt;&lt;p&gt;整个过程中没有一台属于我自己的服务器。&lt;/p&gt;&lt;/section&gt;&lt;section&gt;&lt;h4&gt;根域名和 www&lt;a href=&quot;#根域名和-www&quot;&gt;&lt;span&gt;#&lt;/span&gt;&lt;/a&gt;&lt;/h4&gt;&lt;p&gt;&lt;code&gt;tracerzzz.cn&lt;/code&gt;（根域名，主机记录 &lt;code&gt;@&lt;/code&gt;）和 &lt;code&gt;www.tracerzzz.cn&lt;/code&gt;（子域名，主机记录 &lt;code&gt;www&lt;/code&gt;）在 DNS 里是两个独立的域名，解析、证书都要分别配置。博客的正式地址是不带 www 的那个，&lt;code&gt;www&lt;/code&gt; 也绑到同一个 Pages 项目上，两个地址打开的是同一个网站。本来打算再加一条 301 规则把 &lt;code&gt;www&lt;/code&gt; 跳到根域名，后来觉得没必要：文章里的 canonical 链接和 RSS 写的都是根域名，搜索引擎会按根域名收录，&lt;code&gt;www&lt;/code&gt; 能打开就够了。&lt;/p&gt;&lt;/section&gt;&lt;section&gt;&lt;h4&gt;HTTPS&lt;a href=&quot;#https&quot;&gt;&lt;span&gt;#&lt;/span&gt;&lt;/a&gt;&lt;/h4&gt;&lt;p&gt;ESA 免费版自带 Let’s Encrypt 免费证书，自动续期。申请到的是泛域名证书，证书里登记的域名（SAN）同时包含 &lt;code&gt;*.tracerzzz.cn&lt;/code&gt; 和 &lt;code&gt;tracerzzz.cn&lt;/code&gt;。这一点要确认：&lt;strong&gt;泛域名 &lt;code&gt;*.tracerzzz.cn&lt;/code&gt; 本身不包含根域名&lt;/strong&gt;，&lt;code&gt;*&lt;/code&gt; 只匹配”某个前缀 + 点”，有些平台签发时不会自动带上根域名。&lt;/p&gt;&lt;p&gt;还开了两个选项：&lt;/p&gt;&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;强制 HTTPS&lt;/strong&gt;：http 请求用 301 跳转到 https。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;OCSP Stapling&lt;/strong&gt;：浏览器要确认证书没被吊销，不开的话得自己去问证书颁发机构。开了以后，ESA 提前准备好”证书有效”的证明，随证书一起发给浏览器，首次连接更快。&lt;/li&gt;
&lt;/ul&gt;&lt;p&gt;HSTS 暂时没开：它会让浏览器在设定的时长内只认 HTTPS，一旦配置有误很难撤回，等稳定运行一段时间再说。&lt;/p&gt;&lt;/section&gt;&lt;/section&gt;&lt;/section&gt;
&lt;section&gt;&lt;h2&gt;十二、换主题：从 Retypeset 到 Firefly&lt;a href=&quot;#十二换主题从-retypeset-到-firefly&quot;&gt;&lt;span&gt;#&lt;/span&gt;&lt;/a&gt;&lt;/h2&gt;&lt;section&gt;&lt;h3&gt;为什么能换得这么轻松&lt;a href=&quot;#为什么能换得这么轻松&quot;&gt;&lt;span&gt;#&lt;/span&gt;&lt;/a&gt;&lt;/h3&gt;&lt;p&gt;前面的结构从一开始就和主题无关：文章和图片在 &lt;code&gt;src/content/posts/&lt;/code&gt;，旧链接跳转页在 &lt;code&gt;public/&lt;/code&gt;，欢迎页是独立页面，ESA 配置和检查脚本也和主题没关系。真正要处理的只有两件事：&lt;strong&gt;文章开头的字段名&lt;/strong&gt;，和&lt;strong&gt;文章网址的生成规则&lt;/strong&gt;。&lt;/p&gt;&lt;p&gt;换主题时没有直接在 &lt;code&gt;master&lt;/code&gt; 上改，而是开了一个 &lt;code&gt;theme-firefly&lt;/code&gt; 分支：把 Firefly 的代码放进去，再把文章、图片、跳转页、欢迎页、ESA 配置、检查脚本搬回来。GitHub Actions 会构建这个分支、跑检查，但不发布；本地 &lt;code&gt;pnpm dev&lt;/code&gt; 看满意了，再合并到 &lt;code&gt;master&lt;/code&gt;。&lt;/p&gt;&lt;/section&gt;&lt;section&gt;&lt;h3&gt;保持网址不变：从 &lt;code&gt;abbrlink&lt;/code&gt; 换成 &lt;code&gt;slug&lt;/code&gt;&lt;a href=&quot;#保持网址不变从-abbrlink-换成-slug&quot;&gt;&lt;span&gt;#&lt;/span&gt;&lt;/a&gt;&lt;/h3&gt;&lt;p&gt;Firefly 用文件路径生成文章网址，比如 &lt;code&gt;git/git常用命令.md&lt;/code&gt; 会变成 &lt;code&gt;/posts/git/git常用命令/&lt;/code&gt;。直接这样用的话，所有网址都会变，旧链接跳转页也全部失效。&lt;/p&gt;&lt;p&gt;&lt;strong&gt;第一版做法&lt;/strong&gt;：沿用 Retypeset 时代的 &lt;code&gt;abbrlink&lt;/code&gt; 字段，在内容集合里自定义文章 ID：&lt;/p&gt;&lt;div&gt;&lt;figure&gt;&lt;figcaption&gt;&lt;/figcaption&gt;&lt;pre&gt;&lt;code&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;1&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;&lt;span&gt;loader&lt;/span&gt;&lt;span&gt;: &lt;/span&gt;&lt;/span&gt;&lt;span&gt;glob&lt;/span&gt;&lt;span&gt;({&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;2&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;  &lt;/span&gt;&lt;span&gt;pattern&lt;/span&gt;&lt;span&gt;:&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;&quot;**/*.{md,mdx}&quot;&lt;/span&gt;&lt;span&gt;,&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;3&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;  &lt;/span&gt;&lt;span&gt;base&lt;/span&gt;&lt;span&gt;:&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;&quot;./src/content/posts&quot;&lt;/span&gt;&lt;span&gt;,&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;4&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;  &lt;/span&gt;&lt;span&gt;// 有 abbrlink 就用 abbrlink 当 ID，没有才用文件路径&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;5&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;  &lt;/span&gt;&lt;span&gt;generateId&lt;/span&gt;&lt;span&gt;:&lt;/span&gt;&lt;span&gt;&lt;span&gt; ({ &lt;/span&gt;&lt;span&gt;entry&lt;/span&gt;&lt;span&gt;, &lt;/span&gt;&lt;span&gt;data&lt;/span&gt;&lt;span&gt; }) &lt;/span&gt;&lt;/span&gt;&lt;span&gt;=&amp;gt;&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;6&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;    &lt;/span&gt;&lt;span&gt;typeof&lt;/span&gt;&lt;span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;data&lt;/span&gt;&lt;span&gt;.&lt;/span&gt;&lt;/span&gt;&lt;span&gt;abbrlink&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;===&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;&quot;string&quot;&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;&amp;amp;&amp;amp;&lt;/span&gt;&lt;span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;data&lt;/span&gt;&lt;span&gt;.&lt;/span&gt;&lt;/span&gt;&lt;span&gt;abbrlink&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;7&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;      &lt;/span&gt;&lt;span&gt;?&lt;/span&gt;&lt;span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;data&lt;/span&gt;&lt;span&gt;.&lt;/span&gt;&lt;/span&gt;&lt;span&gt;abbrlink&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;8&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;      &lt;/span&gt;&lt;span&gt;:&lt;/span&gt;&lt;span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;entry&lt;/span&gt;&lt;span&gt;.&lt;/span&gt;&lt;/span&gt;&lt;span&gt;replace&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;&lt;span&gt;/&lt;/span&gt;&lt;span&gt;\.&lt;/span&gt;&lt;span&gt;(md&lt;/span&gt;&lt;span&gt;|&lt;/span&gt;&lt;span&gt;mdx)&lt;/span&gt;&lt;/span&gt;&lt;span&gt;$&lt;/span&gt;&lt;span&gt;/&lt;/span&gt;&lt;span&gt;i&lt;/span&gt;&lt;span&gt;, &lt;/span&gt;&lt;span&gt;&quot;&quot;&lt;/span&gt;&lt;span&gt;),&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;9&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;}),&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;/code&gt;&lt;/pre&gt;&lt;div&gt;&lt;div&gt;&lt;/div&gt;&lt;div&gt;&lt;/div&gt;&lt;/div&gt;&lt;/figure&gt;&lt;/div&gt;&lt;p&gt;主题里所有生成文章链接的地方用的都是这个 ID，所以网址都变回了 &lt;code&gt;/posts/git-commands/&lt;/code&gt;。能用，但这是改了主题的代码，以后升级主题时，这几行可能会被覆盖，或者和新版本冲突。&lt;/p&gt;&lt;p&gt;&lt;strong&gt;现在的做法&lt;/strong&gt;：后来发现根本不用改代码。Astro 的 &lt;code&gt;glob&lt;/code&gt; 加载器本身就有这个规则：文章开头写了 &lt;code&gt;slug&lt;/code&gt;，就用它当 ID；没写，才用文件路径。Firefly 自带的 &lt;code&gt;pnpm new-post&lt;/code&gt; 生成的也是 &lt;code&gt;slug&lt;/code&gt; 字段。于是把所有文章的 &lt;code&gt;abbrlink&lt;/code&gt; 改名为 &lt;code&gt;slug&lt;/code&gt;，再删掉上面那段自定义代码，&lt;code&gt;content.config.ts&lt;/code&gt; 就和主题原版一模一样了。网址一个没变。&lt;/p&gt;&lt;p&gt;&lt;code&gt;slug&lt;/code&gt; 只用小写字母、数字和连字符，不要带 &lt;code&gt;/&lt;/code&gt;，带了网址里就会多出一层目录。&lt;/p&gt;&lt;/section&gt;&lt;section&gt;&lt;h3&gt;分类和封面&lt;a href=&quot;#分类和封面&quot;&gt;&lt;span&gt;#&lt;/span&gt;&lt;/a&gt;&lt;/h3&gt;&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;分类&lt;/strong&gt;：用脚本给每篇文章补上 &lt;code&gt;category&lt;/code&gt;，值按所在文件夹来，比如 &lt;code&gt;git/&lt;/code&gt; 对应”Git”，&lt;code&gt;database/&lt;/code&gt; 对应”数据库”。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;封面&lt;/strong&gt;：用正文里的第一张图（后来发现详情页会重复显示，关掉了详情页的封面，见第四节”封面怎么写”）。这里又踩了一次”代码里的示例被当成真图片”的坑：迁移这篇文章里有一句举例用的 &lt;code&gt;![](./文章名.assets/...)&lt;/code&gt;，被脚本当成了封面，构建时找不到这个文件。改成跳过代码块里的内容就好了。&lt;/li&gt;
&lt;/ul&gt;&lt;/section&gt;&lt;section&gt;&lt;h3&gt;进出欢迎页时的残影&lt;a href=&quot;#进出欢迎页时的残影&quot;&gt;&lt;span&gt;#&lt;/span&gt;&lt;/a&gt;&lt;/h3&gt;&lt;p&gt;Firefly 用 swup 做页面过渡：切换页面时不整页刷新，而是只替换页面里几块固定的区域（横幅、正文、侧边栏），再配上淡入淡出。欢迎页是独立页面，没有这几块区域，所以进出欢迎页时只换掉了一部分，剩下的就成了残影。&lt;/p&gt;&lt;p&gt;解决办法是让 swup 跳过欢迎页：&lt;/p&gt;&lt;div&gt;&lt;figure&gt;&lt;figcaption&gt;&lt;/figcaption&gt;&lt;pre&gt;&lt;code&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;1&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;swup&lt;/span&gt;&lt;span&gt;({&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;2&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;  &lt;/span&gt;&lt;span&gt;// ...&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;3&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;  &lt;/span&gt;&lt;span&gt;ignore&lt;/span&gt;&lt;span&gt;:&lt;/span&gt;&lt;span&gt;&lt;span&gt; (&lt;/span&gt;&lt;span&gt;url&lt;/span&gt;&lt;span&gt;) &lt;/span&gt;&lt;/span&gt;&lt;span&gt;=&amp;gt;&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;4&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;    &lt;/span&gt;&lt;span&gt;new&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;URL&lt;/span&gt;&lt;span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;url&lt;/span&gt;&lt;span&gt;, &lt;/span&gt;&lt;span&gt;window&lt;/span&gt;&lt;span&gt;.&lt;/span&gt;&lt;/span&gt;&lt;span&gt;location&lt;/span&gt;&lt;span&gt;.&lt;/span&gt;&lt;span&gt;href&lt;/span&gt;&lt;span&gt;).&lt;/span&gt;&lt;span&gt;pathname&lt;/span&gt;&lt;span&gt;.&lt;/span&gt;&lt;span&gt;startsWith&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;&quot;/welcome&quot;&lt;/span&gt;&lt;span&gt;) &lt;/span&gt;&lt;span&gt;||&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;5&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;&lt;span&gt;    &lt;/span&gt;&lt;/span&gt;&lt;span&gt;window&lt;/span&gt;&lt;span&gt;.&lt;/span&gt;&lt;span&gt;location&lt;/span&gt;&lt;span&gt;.&lt;/span&gt;&lt;span&gt;pathname&lt;/span&gt;&lt;span&gt;.&lt;/span&gt;&lt;span&gt;startsWith&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;&quot;/welcome&quot;&lt;/span&gt;&lt;span&gt;),&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;6&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;}),&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;/code&gt;&lt;/pre&gt;&lt;div&gt;&lt;div&gt;&lt;/div&gt;&lt;div&gt;&lt;/div&gt;&lt;/div&gt;&lt;/figure&gt;&lt;/div&gt;&lt;p&gt;目标是欢迎页，或者当前就在欢迎页，都按普通方式整页跳转；博客内部各页面之间照样有过渡动画。&lt;/p&gt;&lt;blockquote&gt;&lt;p&gt;后来为了让音乐在进出欢迎页时不中断，把欢迎页套进了博客的页面框架，残影的根源没了，这条规则也随之删掉，见下面”音乐”一段。&lt;/p&gt;&lt;/blockquote&gt;&lt;/section&gt;&lt;section&gt;&lt;h3&gt;分页后横幅位置不一样&lt;a href=&quot;#分页后横幅位置不一样&quot;&gt;&lt;span&gt;#&lt;/span&gt;&lt;/a&gt;&lt;/h3&gt;&lt;p&gt;文章列表翻到第二页，横幅的壁纸取景和第一页不一样。原因是 Firefly 把横幅分成两种：首页的横幅更高、带大标题；其他页面的横幅会整体上移一截，留出更多空间给正文。主题只把 &lt;code&gt;/&lt;/code&gt; 当首页，第二页的地址是 &lt;code&gt;/2/&lt;/code&gt;，被当成了普通页面。&lt;/p&gt;&lt;p&gt;把分页地址（&lt;code&gt;/2/&lt;/code&gt;、&lt;code&gt;/3/&lt;/code&gt;……）也算作首页就好了。这个判断主题里有三处：生成页面时一处，浏览器里 swup 切换页面时两处。三处都要改，否则直接打开第二页正常，从第一页点过去又会不一样。&lt;/p&gt;&lt;/section&gt;&lt;section&gt;&lt;h3&gt;备用站上的分享链接还是主站域名&lt;a href=&quot;#备用站上的分享链接还是主站域名&quot;&gt;&lt;span&gt;#&lt;/span&gt;&lt;/a&gt;&lt;/h3&gt;&lt;p&gt;在 &lt;code&gt;tracerzzz.github.io&lt;/code&gt; 上点分享，复制出来的却是 &lt;code&gt;tracerzzz.cn&lt;/code&gt; 的地址。原因是网站是同一份代码，两边各构建一次，构建时文章的完整地址都按配置里的主域名写进了页面。&lt;/p&gt;&lt;p&gt;修法是在浏览器里打开页面时，把分享用的地址换成当前访问的域名：&lt;/p&gt;&lt;div&gt;&lt;figure&gt;&lt;figcaption&gt;&lt;/figcaption&gt;&lt;pre&gt;&lt;code&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;1&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;function&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;shareUrl&lt;/span&gt;&lt;span&gt;()&lt;/span&gt;&lt;span&gt;:&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;string&lt;/span&gt;&lt;span&gt; {&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;2&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;  &lt;/span&gt;&lt;span&gt;const&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;u&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;=&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;new&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;URL&lt;/span&gt;&lt;span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;url&lt;/span&gt;&lt;span&gt;, &lt;/span&gt;&lt;span&gt;window&lt;/span&gt;&lt;span&gt;.&lt;/span&gt;&lt;/span&gt;&lt;span&gt;location&lt;/span&gt;&lt;span&gt;.&lt;/span&gt;&lt;span&gt;href&lt;/span&gt;&lt;span&gt;);&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;3&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;  &lt;/span&gt;&lt;span&gt;return&lt;/span&gt;&lt;span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;window&lt;/span&gt;&lt;span&gt;.&lt;/span&gt;&lt;/span&gt;&lt;span&gt;location&lt;/span&gt;&lt;span&gt;.&lt;/span&gt;&lt;span&gt;origin&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;+&lt;/span&gt;&lt;span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;u&lt;/span&gt;&lt;span&gt;.&lt;/span&gt;&lt;/span&gt;&lt;span&gt;pathname&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;+&lt;/span&gt;&lt;span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;u&lt;/span&gt;&lt;span&gt;.&lt;/span&gt;&lt;/span&gt;&lt;span&gt;search&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;+&lt;/span&gt;&lt;span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;u&lt;/span&gt;&lt;span&gt;.&lt;/span&gt;&lt;/span&gt;&lt;span&gt;hash&lt;/span&gt;&lt;span&gt;;&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;4&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;}&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;/code&gt;&lt;/pre&gt;&lt;div&gt;&lt;div&gt;&lt;/div&gt;&lt;div&gt;&lt;/div&gt;&lt;/div&gt;&lt;/figure&gt;&lt;/div&gt;&lt;p&gt;一共三处：分享弹窗里的”复制链接”、分享海报上的二维码、文章底部版权信息里的”本文链接”。&lt;/p&gt;&lt;p&gt;页面里给搜索引擎看的”正式地址”（&lt;code&gt;&amp;lt;link rel=&quot;canonical&quot;&amp;gt;&lt;/code&gt;）和 RSS 里的链接&lt;strong&gt;故意没改&lt;/strong&gt;，还是指向 &lt;code&gt;tracerzzz.cn&lt;/code&gt;。两个站点内容完全一样，这样搜索引擎只会收录主站，不会把备用站当成重复内容。&lt;/p&gt;&lt;/section&gt;&lt;section&gt;&lt;h3&gt;音乐：从”跳到网易云主页”到”全站切换不断播”&lt;a href=&quot;#音乐从跳到网易云主页到全站切换不断播&quot;&gt;&lt;span&gt;#&lt;/span&gt;&lt;/a&gt;&lt;/h3&gt;&lt;p&gt;这一块前后改了六版，每一版都是被上一版的问题逼出来的。&lt;/p&gt;&lt;p&gt;&lt;strong&gt;1. 原来的链接是死链。&lt;/strong&gt; 欢迎页的音乐按钮链接到我的网易云个人主页。用没登录的浏览器打开，直接是 404。给别人看等于没有。&lt;/p&gt;&lt;p&gt;&lt;strong&gt;2. 网易云外链播放器 + 弹窗。&lt;/strong&gt; 网易云提供官方的外链播放器（&lt;code&gt;music.163.com/outchain/player?type=0&amp;amp;id=歌单ID&lt;/code&gt;），不用登录就能播放公开歌单。先做成点击音乐图标弹出一个窗口，关闭窗口时把播放器整个移除，音乐也就停了。问题是弹窗一直挡在页面中间，不友好。外链播放器还放不了 VIP 歌曲，歌单里只有免费的歌能完整播放。&lt;/p&gt;&lt;p&gt;&lt;strong&gt;3. 改成悬停浮出。&lt;/strong&gt; 鼠标移到音乐图标上，旁边浮出一个小播放器；移开后隐藏，音乐继续放。几个细节：&lt;/p&gt;&lt;ul&gt;
&lt;li&gt;鼠标移开后延迟 0.4 秒再隐藏，否则从图标移向播放器的途中就消失了。&lt;/li&gt;
&lt;li&gt;隐藏用 &lt;code&gt;visibility: hidden&lt;/code&gt;，不用 &lt;code&gt;display: none&lt;/code&gt;，也不移除 iframe，保证声音不断。&lt;/li&gt;
&lt;li&gt;手机上没有悬停，改成点一下显示、再点一下隐藏。&lt;/li&gt;
&lt;li&gt;浮窗做成半透明的毛玻璃（&lt;code&gt;backdrop-filter: blur()&lt;/code&gt;），边框换成很淡的白线。中间那条播放条的底色是网易云页面决定的，跨域改不了。&lt;/li&gt;
&lt;/ul&gt;&lt;p&gt;&lt;strong&gt;4. 想显示”正在播放”，碰上了同源策略。&lt;/strong&gt; 播放器在 iframe 里，地址是 &lt;code&gt;music.163.com&lt;/code&gt;，外面的欢迎页是 &lt;code&gt;tracerzzz.cn&lt;/code&gt;。浏览器的同源策略禁止一个页面读取另一个域名页面的内容，我们拿不到它的播放状态、当前歌曲，也收不到播放和暂停事件。跨域页面之间唯一正规的通信方式是 &lt;code&gt;postMessage&lt;/code&gt;，但这需要网易云在播放器里主动发消息，它没有提供这样的接口。&lt;/p&gt;&lt;p&gt;不过网易云播放器的完整版自带当前歌曲和歌单列表。用了一个小技巧来加”播放列表”按钮：始终嵌入完整版，平时用外框裁到只露出顶部的播放条；点按钮时把外框拉高，露出下面的歌单。因为一直是同一个 iframe，展开、收起时音乐不会中断。&lt;/p&gt;&lt;p&gt;&lt;strong&gt;5. 博客里也要能听，而且要和欢迎页共享进度。&lt;/strong&gt; Firefly 自带一个音乐播放器，能通过 Meting 接口读取网易云歌单，在导航栏和侧边栏播放。Meting 是第三方搭的接口，主题里配了几个备用地址，一个失效会自动换下一个；VIP 歌曲会被接口跳过。只改了配置文件（歌单 ID 和开关），没改主题代码。博客页面之间切换时音乐不中断，因为 swup 只替换正文，播放器所在的部分一直保留着。&lt;/p&gt;&lt;p&gt;欢迎页的外链播放器在跨域的 iframe 里，读不到进度，没法和博客共享，所以欢迎页也改用了同一个播放器（主题的 &lt;code&gt;MusicManager&lt;/code&gt;），另做了一个迷你界面：封面、歌名、进度条、上一首 / 播放 / 下一首、可展开的歌单。界面通过主题播放器公开的 &lt;code&gt;window.__fireflyMusic&lt;/code&gt; 接口控制，监听它发出的 &lt;code&gt;fm:track&lt;/code&gt;、&lt;code&gt;fm:time&lt;/code&gt; 等事件更新显示。&lt;/p&gt;&lt;p&gt;但欢迎页当时还是独立页面，进出欢迎页都是整页刷新，音乐会断。于是加了 &lt;code&gt;music-resume.ts&lt;/code&gt;：离开页面（&lt;code&gt;pagehide&lt;/code&gt;）前把”第几首、播到第几秒、是否在播放”存进 &lt;code&gt;sessionStorage&lt;/code&gt;，新页面的播放器就绪后跳到同一首、同一位置继续。这里有个时序坑：主题播放器初始化时先发 &lt;code&gt;fm:init&lt;/code&gt; 事件、再加载第一首，如果在 &lt;code&gt;fm:init&lt;/code&gt; 里马上恢复，会被它紧接着的”加载第一首”覆盖，所以要晚一拍再恢复。&lt;/p&gt;&lt;p&gt;&lt;strong&gt;6. 还是会卡一下，最后让欢迎页”不换页”。&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;弹出一个独立的播放器小窗口（&lt;code&gt;window.open&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;预渲染（Speculation Rules）+ 缓存音频地址 + 淡入&lt;/td&gt;&lt;td&gt;不能，只是停顿变短&lt;/td&gt;&lt;td&gt;只有 Chrome 系浏览器支持预渲染&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;strong&gt;把欢迎页整个套进博客的页面框架&lt;/strong&gt;&lt;/td&gt;&lt;td&gt;&lt;strong&gt;能&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;ul&gt;
&lt;li&gt;&lt;code&gt;welcome.astro&lt;/code&gt; 改成和”关于”页一样用主题的 &lt;code&gt;MainGridLayout&lt;/code&gt;，原来的欢迎页内容不拆，整体放进一个铺满全屏、盖在最上层的图层 &lt;code&gt;#welcome-root&lt;/code&gt; 里。播放器在页面框架里，swup 切换时只换内容区域，播放器不销毁，音乐完全不断。&lt;/li&gt;
&lt;li&gt;残影的根源（欢迎页没有 swup 需要的容器）也随之消失，前面那条”欢迎页不走 swup”的规则删掉了，对主题配置的修改反而少了一处。&lt;/li&gt;
&lt;li&gt;swup 切换时不会重新执行页面脚本，所以欢迎页的动画改成了 &lt;code&gt;mountWelcome()&lt;/code&gt;：进入时启动，返回一个清理函数；离开时停掉时间线和定时器，并用 &lt;code&gt;AbortController&lt;/code&gt; 一次性移除挂在 &lt;code&gt;window&lt;/code&gt;、&lt;code&gt;document&lt;/code&gt; 上的监听。由一个注入到每个页面的 &lt;code&gt;controller.ts&lt;/code&gt; 负责调用：新内容里出现 &lt;code&gt;#welcome-root&lt;/code&gt; 就挂载；离开时先淡出，新内容换上后再卸载。&lt;/li&gt;
&lt;li&gt;&lt;code&gt;#welcome-root&lt;/code&gt; 挂载时会被移到 &lt;code&gt;&amp;lt;body&amp;gt;&lt;/code&gt; 下面。因为 &lt;code&gt;position: fixed&lt;/code&gt; 的元素，如果某个祖先设置了 &lt;code&gt;transform&lt;/code&gt;，就会相对那个祖先定位，而不是相对窗口；主题切换页面时会给内容区域加 &lt;code&gt;transform&lt;/code&gt; 做动画。&lt;/li&gt;
&lt;li&gt;样式全部限定在 &lt;code&gt;#welcome-root&lt;/code&gt; 里。原来针对 &lt;code&gt;html, body&lt;/code&gt; 的”禁止滚动”改成 &lt;code&gt;html:has(#welcome-root) { overflow: hidden }&lt;/code&gt;：欢迎页在的时候生效，换走了自然失效，不用手动恢复。几个通用的元素 ID（&lt;code&gt;#main&lt;/code&gt;、&lt;code&gt;#menu&lt;/code&gt; 等）加了 &lt;code&gt;wl-&lt;/code&gt; 前缀，避免和主题冲突。&lt;/li&gt;
&lt;li&gt;被盖住的博客页面会在后台照常加载。看起来多加载了东西，其实相当于提前把博客的样式和壁纸缓存好了：访客在欢迎页看动画的时候它们就加载完了，点进博客反而更快。&lt;/li&gt;
&lt;li&gt;又踩了一个时序坑：&lt;code&gt;controller.ts&lt;/code&gt; 要等 swup 初始化好才能注册钩子，而主题的 swup 是在页面空闲时才初始化的，比这个脚本晚。光监听 &lt;code&gt;swup:enable&lt;/code&gt; 事件有时会错过，最后加了定时检查兜底。&lt;/li&gt;
&lt;/ul&gt;&lt;p&gt;欢迎页上的 GitHub 按钮是外部链接，在当前页面打开会把整个页面换掉，音乐也就停了，所以改成在新标签页打开（&lt;code&gt;target=&quot;_blank&quot;&lt;/code&gt;）。博客导航栏和横幅上的 GitHub 链接，主题本来就是在新标签页打开的。&lt;/p&gt;&lt;p&gt;现在只有按 F5 刷新页面才会整页重新加载，&lt;code&gt;music-resume.ts&lt;/code&gt; 留着处理这种情况，会停一两秒再接着放。浏览器的自动播放策略也要考虑：大多数情况下 Chrome 允许接着放，Safari 可能拦下来，这时点一下播放，会从原来的位置继续。&lt;/p&gt;&lt;/section&gt;&lt;section&gt;&lt;h3&gt;进入欢迎页时博客一闪而过&lt;a href=&quot;#进入欢迎页时博客一闪而过&quot;&gt;&lt;span&gt;#&lt;/span&gt;&lt;/a&gt;&lt;/h3&gt;&lt;p&gt;欢迎页并进博客的页面框架以后，出现了一个新问题：刷新欢迎页，或者从博客点按钮进欢迎页，都会先看到博客页面一闪，然后才是黑色的欢迎页。原因是欢迎页的黑色图层要等脚本跑完才能铺满窗口，而且铺上以后还从透明开始淡入，这段时间里底下的博客就露出来了。具体分三处：&lt;/p&gt;&lt;ol&gt;
&lt;li&gt;&lt;strong&gt;刷新时，脚本执行前盖不住。&lt;/strong&gt; 欢迎页在 HTML 里位于主题的 &lt;code&gt;#content-wrapper&lt;/code&gt; 里面，这个元素带主题的入场动画：一开始透明度为 0，还有 &lt;code&gt;transform&lt;/code&gt; 位移。里面的欢迎页跟着透明，而且前面说过，祖先有 &lt;code&gt;transform&lt;/code&gt; 时 &lt;code&gt;position: fixed&lt;/code&gt; 相对祖先定位，只能盖住中间的内容栏。把欢迎页移到 &lt;code&gt;&amp;lt;body&amp;gt;&lt;/code&gt; 下的 &lt;code&gt;controller.ts&lt;/code&gt; 是模块脚本，要等 HTML 解析完才执行，在这之前导航栏、横幅壁纸已经画出来了。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;从博客切换过来时，下载期间什么都没盖。&lt;/strong&gt; swup 先把旧内容淡出、去下载欢迎页，导航栏和横幅一直在；内容换上后，因为欢迎页不算首页，主题还会把横幅从首页的高度收成普通页面的高度。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;欢迎页自己从透明淡入。&lt;/strong&gt; &lt;code&gt;#welcome-root&lt;/code&gt; 上有 0.3 秒的淡入，淡入时透出底下正在变化的博客。&lt;/li&gt;
&lt;/ol&gt;&lt;p&gt;对应的改法，都在自己的文件里，没碰主题代码：&lt;/p&gt;&lt;ul&gt;
&lt;li&gt;&lt;code&gt;#welcome-root&lt;/code&gt; 的第一个子元素是一小段内联脚本（&lt;code&gt;&amp;lt;script is:inline&amp;gt;&lt;/code&gt;），浏览器一解析到这里，就用 &lt;code&gt;document.currentScript.parentElement&lt;/code&gt; 找到欢迎页、移到 &lt;code&gt;&amp;lt;body&amp;gt;&lt;/code&gt; 下，不等模块脚本。放在最前面而不是最后面，是因为欢迎页里有 300KB 的内联 SVG，放最后要等它们都解析完。元素移走后，浏览器后面解析出的内容照样会放进这个元素里。同时加了一条 CSS：&lt;code&gt;#content-wrapper:has(#welcome-root)&lt;/code&gt; 关掉入场动画和 &lt;code&gt;transform&lt;/code&gt;，双保险。这条规则只在欢迎页还在内容区里的那一瞬间生效；一开始写成了 &lt;code&gt;html:has(#welcome-root) #content-wrapper&lt;/code&gt;，范围太大，欢迎页移走后也一直生效。&lt;/li&gt;
&lt;li&gt;黑色背景一开始就不透明，淡入只作用在里面的内容（&lt;code&gt;#wl-main&lt;/code&gt;）上。&lt;/li&gt;
&lt;li&gt;&lt;code&gt;controller.ts&lt;/code&gt; 在 swup 的 &lt;code&gt;visit:start&lt;/code&gt; 里判断：如果要去欢迎页，立刻盖上一层黑色遮罩，挡住下载等待和横幅收缩；欢迎页铺满窗口后撤掉，&lt;code&gt;visit:end&lt;/code&gt; 时再检查一次，保证切换失败时遮罩不会一直留着。遮罩的样式直接写在元素上，因为在博客页面上还没加载欢迎页的样式表。&lt;/li&gt;
&lt;/ul&gt;&lt;/section&gt;&lt;section&gt;&lt;h3&gt;花瓶的线条不”长”了&lt;a href=&quot;#花瓶的线条不长了&quot;&gt;&lt;span&gt;#&lt;/span&gt;&lt;/a&gt;&lt;/h3&gt;&lt;p&gt;上线后对比旧版，发现左边的花瓶少了一个效果：旧版是线条沿着轮廓慢慢”长”出来，新版只是整体淡入，看着发虚。&lt;/p&gt;&lt;p&gt;查了一下，是移植时漏掉了一行。旧代码里花瓶有两组动画：&lt;code&gt;#gflower&lt;/code&gt; 的透明度和颜色变化，以及 &lt;code&gt;drawToTimeline(&quot;#flower&quot;, tl, { duration: 38, strokeWidth: 1, position: 5 })&lt;/code&gt;，后者从第 5 秒开始、用 38 秒把描边一点点画出来。新版只搬了前一组。&lt;/p&gt;&lt;p&gt;描边”生长”的原理是 SVG 的虚线属性：&lt;code&gt;stroke-dasharray: 已画长度 剩余长度&lt;/code&gt;。用 &lt;code&gt;getTotalLength()&lt;/code&gt; 拿到路径总长 L，从 &lt;code&gt;0 L&lt;/code&gt;（实线段长 0，全是空白）补间到 &lt;code&gt;L 0&lt;/code&gt;（整条都是实线），线条就像沿着路径画出来一样。花瓶整幅图其实是一条 260KB 的 &lt;code&gt;&amp;lt;path&amp;gt;&lt;/code&gt;，所以是从一处开始，沿着轮廓一路画下去。&lt;/p&gt;&lt;p&gt;补回这一行时还顺手修了补间引擎的一个问题：描边动画原来用的是 &lt;code&gt;to&lt;/code&gt;，开始之前读到的起始值是”已画完”，于是开始前会先显示完整的描边，到点才清空再画；每轮循环重新开始时也一样。改成 &lt;code&gt;fromTo&lt;/code&gt; 并明确起点 &lt;code&gt;draw: 0&lt;/code&gt; 后，开始之前一直保持”还没画”的状态。右边圆形图案的描边也跟着受益。&lt;/p&gt;&lt;/section&gt;&lt;section&gt;&lt;h3&gt;评论：从 Gitment 到 Giscus&lt;a href=&quot;#评论从-gitment-到-giscus&quot;&gt;&lt;span&gt;#&lt;/span&gt;&lt;/a&gt;&lt;/h3&gt;&lt;p&gt;旧博客用的是 Gitment：每篇文章对应仓库里的一个 GitHub Issue，访客用 GitHub 账号登录后评论，评论就是 Issue 下面的回复。翻了一下，当年每篇文章都建了 Issue，真正有评论的只有两篇，各一条，这次就不迁了。Gitment 本身早就不能用了，它依赖作者搭的一个登录中转服务，那个服务已经停了。&lt;/p&gt;&lt;p&gt;Firefly 内置了五种评论系统，改 &lt;code&gt;src/config/commentConfig.ts&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;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;Giscus&lt;/td&gt;&lt;td&gt;仓库的 GitHub Discussions&lt;/td&gt;&lt;td&gt;要，GitHub 账号&lt;/td&gt;&lt;td&gt;不用&lt;/td&gt;&lt;td&gt;能用，偶尔慢&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;Twikoo&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;Waline&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;Artalk&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;Disqus&lt;/td&gt;&lt;td&gt;Disqus 公司&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;Giscus&lt;/strong&gt;。它和 Gitment 是一个思路，借 GitHub 存评论，只是从 Issues 换成了 Discussions。不用部署后端，不花钱，数据在自己仓库里，不怕第三方服务关停；技术博客的读者大多也有 GitHub 账号。代价是没有 GitHub 账号的人没法评论，想让任何人都能匿名评论，就得换 Twikoo 这类要自己搭后端的方案。&lt;/p&gt;&lt;p&gt;配置步骤：&lt;/p&gt;&lt;ol&gt;
&lt;li&gt;仓库 Settings → General → Features 里打开 &lt;strong&gt;Discussions&lt;/strong&gt;。Giscus 要求仓库是公开的，前面为了 GitHub Pages 已经改成公开了。&lt;/li&gt;
&lt;li&gt;安装 Giscus 的 GitHub App，只授权这一个仓库。&lt;/li&gt;
&lt;li&gt;在 &lt;code&gt;commentConfig.ts&lt;/code&gt; 里把 &lt;code&gt;type&lt;/code&gt; 改成 &lt;code&gt;giscus&lt;/code&gt;，填上仓库名、仓库 ID、分类和分类 ID。分类用的是 &lt;strong&gt;Announcements&lt;/strong&gt;，这是 Giscus 推荐的：这个分类只有仓库管理员和 Giscus 能新建讨论，别人没法在里面乱发帖。&lt;/li&gt;
&lt;li&gt;&lt;code&gt;mapping&lt;/code&gt; 用 &lt;code&gt;pathname&lt;/code&gt;：按文章网址找对应的讨论。主站和备用站的同一篇文章网址路径一样，所以两边共用一组评论；网址是 &lt;code&gt;slug&lt;/code&gt; 决定的、不会变，评论也就不会对不上。&lt;/li&gt;
&lt;/ol&gt;&lt;p&gt;仓库 ID 和分类 ID 一般是去 giscus.app 填仓库名自动生成的。这次是直接从 GitHub 的接口查到的：仓库信息里的 &lt;code&gt;node_id&lt;/code&gt; 就是仓库 ID，分类 ID 从开启 Discussions 时自动生成的那条欢迎帖里能看到。&lt;/p&gt;&lt;p&gt;顺带处理了一个安全问题：旧的 Hexo 配置文件里写着 Gitment 用的 GitHub OAuth 应用密钥（&lt;code&gt;client_secret&lt;/code&gt;），它在 &lt;code&gt;hexoWorkspace&lt;/code&gt; 仓库里，也被打包进了 &lt;code&gt;hexo-site&lt;/code&gt; 分支的每个页面。仓库公开后谁都能看到，所以把那个 OAuth 应用删掉了。教训是：&lt;strong&gt;往仓库里提交配置前，先确认里面没有密钥&lt;/strong&gt;。前端页面里出现的东西，就当作是公开的。&lt;/p&gt;&lt;/section&gt;&lt;section&gt;&lt;h3&gt;其他小事&lt;a href=&quot;#其他小事&quot;&gt;&lt;span&gt;#&lt;/span&gt;&lt;/a&gt;&lt;/h3&gt;&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;横幅壁纸&lt;/strong&gt;换成了饥荒联机版的加载界面，开了轮播，淡入淡出切换。头像换回了原来 Hexo 博客用的柴犬。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;壁纸换成了无损原图&lt;/strong&gt;：两张壁纸是游戏里的加载界面 &lt;em&gt;Open the Gateway&lt;/em&gt; 和 &lt;em&gt;Axiom Visus&lt;/em&gt;。本来想换 4K 的，查了一圈发现没有：官方动画短片在 YouTube 上最高只有 1080p；游戏文件里的加载界面原图也就 1920×1080 和 1820×1024，饥荒 Wiki 上收录的一百多张加载界面里，最大就是 1920×1080，网上标着 4K 的都是放大的。能改进的只有画质：原来用的是有损压缩过的 WebP，主题构建时还会再压一次；换成 Wiki 上从游戏里提取的无损原图（转成无损 WebP 存进仓库，像素完全一致，文件比 PNG 小），构建时只压一次。主题在电脑上最大只输出 1920 宽的壁纸，所以原图有 1920 宽就够了，再大也用不上。
&lt;ul&gt;
&lt;li&gt;一个小插曲：Wiki 会根据浏览器支持的格式返回图片，网址是 &lt;code&gt;.png&lt;/code&gt;，用浏览器”存储图像为”存下来的却可能是 WebP。文件头是 &lt;code&gt;RIFF....WEBPVP8L&lt;/code&gt;，&lt;code&gt;VP8L&lt;/code&gt; 表示无损 WebP；如果是 &lt;code&gt;VP8 &lt;/code&gt;（后面带空格）就是有损的。&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;关掉了用不上的功能&lt;/strong&gt;：友链、留言板、动态、相册、公告、打赏、看板娘。在 &lt;code&gt;src/config/siteConfig.ts&lt;/code&gt; 里都是一个开关。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;删掉了主题仓库自带的 GitHub 配置&lt;/strong&gt;：Issue 模板、赞助按钮、Dependabot。Dependabot 是给主题作者自己的仓库自动升级依赖用的，留着它会在我的仓库里不停建分支。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;&lt;code&gt;pnpm new-post&lt;/code&gt; 也改了&lt;/strong&gt;：生成的 &lt;code&gt;slug&lt;/code&gt; 只取文件名部分（不带文件夹），日期带上 &lt;code&gt;+08:00&lt;/code&gt; 时区。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;文章日期加了文字&lt;/strong&gt;：主题在文章页标题下会同时显示发布日期和更新日期，但只用两个图标区分，并排放着分不清。改了 &lt;code&gt;PostMeta.astro&lt;/code&gt;，在日期前加上「发布于」「更新于」（主题自带这两条文案，直接复用）。首页列表卡片本来就不显示更新日期，保持不变。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;点 PDF 链接回不去&lt;/strong&gt;：文章里的专利 PDF 链接，点了以后地址栏变成 &lt;code&gt;.pdf&lt;/code&gt;，页面却没换，浏览器返回也乱了。原因是主题用 swup 做无刷新页面切换，它默认拦截所有站内链接，把 PDF 也当成网页去加载。在 &lt;code&gt;astro.config.mjs&lt;/code&gt; 的 swup 配置里加了 &lt;code&gt;ignore&lt;/code&gt;，PDF、压缩包、Office 文档、音视频这类文件交给浏览器正常打开。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;导航栏图标闪一下&lt;/strong&gt;：导航栏的 logo 直接用 &lt;code&gt;favicon.svg&lt;/code&gt;，这个 SVG 只写了 viewBox，没写宽高；&lt;code&gt;&amp;lt;img&amp;gt;&lt;/code&gt; 的尺寸又全靠 CSS 类。整页刷新时，CSS 加载完之前浏览器会按默认尺寸把它画出来，就会看到一个大图标一闪而过。给 &lt;code&gt;&amp;lt;img&amp;gt;&lt;/code&gt; 直接写上 &lt;code&gt;width=&quot;28&quot; height=&quot;28&quot;&lt;/code&gt; 就好了。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;博客名改回朴素的写法&lt;/strong&gt;：原来叫 &lt;code&gt;À Çraftman&lt;/code&gt;，A 和 C 头上加了法语的变音符号，当时觉得有意思，后来看着有点非主流：不好打、不好搜，“Craftman” 本身也拼错了。改成了 &lt;strong&gt;A Craftsman&lt;/strong&gt;（工匠），和描述”想当木匠的程序员！“对得上。站点标题、横幅标题、欢迎页描述、图标注释一起改了。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;页脚补上备案号&lt;/strong&gt;：tracerzzz.cn 在阿里云做过 ICP 备案，按规定网站底部要显示备案号并链接到工信部的备案系统。旧 Hexo 博客的页脚有这一行，迁移时漏掉了。主题留了 &lt;code&gt;src/config/FooterConfig.html&lt;/code&gt; 专门放自定义页脚，加一行 &lt;code&gt;京ICP备18058118号-1&lt;/code&gt; 就行。用的是网站备案号（主体备案号后面带 &lt;code&gt;-1&lt;/code&gt;，旧博客只写了主体号）。旧博客链接的 &lt;code&gt;www.miitbeian.gov.cn&lt;/code&gt; 早已停用，现在的地址是 &lt;code&gt;https://beian.miit.gov.cn/&lt;/code&gt;。链接只能指向首页，不能带上备案号直接跳到查询结果：备案系统是单页应用，查询要先过滑块验证，结果页没有可分享的网址。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;本地预览的小坑&lt;/strong&gt;：切到新主题的分支后，再运行一次 &lt;code&gt;pnpm dev&lt;/code&gt;，如果旧主题的预览服务还开着，新的会自动换到 4322 端口。这时去 4321 端口看到的还是旧主题。&lt;/li&gt;
&lt;/ul&gt;&lt;/section&gt;&lt;section&gt;&lt;h3&gt;网站图标：丁字尺&lt;a href=&quot;#网站图标丁字尺&quot;&gt;&lt;span&gt;#&lt;/span&gt;&lt;/a&gt;&lt;/h3&gt;&lt;p&gt;原来的网站图标（favicon）是 Hexo 时代留下的，导航栏左上角是主题图标库里的锤子扳手，两者也不统一。这次一起换成了自己设计的”丁字尺”：&lt;/p&gt;&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;为什么是丁字尺&lt;/strong&gt;：tracerZzz 的 T，本身就是一件工具。丁字尺是画直线用的 T 形尺子，木工、制图都用，竖杆上有刻度，正好对应博客名 A Craftsman（工匠）。小尺寸下刻度看不见，就是一个干净的 T。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;试过的其他方向&lt;/strong&gt;：榫卯（燕尾榫、银锭榫、透榫，平面和立体都画过）、把 tracerZzz 的 Z 做进榫卯里、一笔画的 T。榫卯最有手艺感，但要让人一眼看出是榫卯，就得画成立体、加细节，到 16px 的标签页图标里全糊了；反而最简单的丁字尺在每个尺寸都清楚。图标设计最后比的是&lt;strong&gt;小尺寸下还认不认得出来&lt;/strong&gt;。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;一套图标三个文件&lt;/strong&gt;，都在 &lt;code&gt;public/&lt;/code&gt; 下：
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;favicon.svg&lt;/code&gt;：矢量图，任意大小都清晰，现代浏览器优先用它；导航栏的 logo 也直接用这个文件。&lt;/li&gt;
&lt;li&gt;&lt;code&gt;favicon.ico&lt;/code&gt;：里面打包了 16、32、48 三种尺寸的位图，给不支持 SVG 图标的老浏览器。&lt;/li&gt;
&lt;li&gt;&lt;code&gt;apple-touch-icon.png&lt;/code&gt;：180×180，苹果设备”添加到主屏幕”时用。这张&lt;strong&gt;不带圆角&lt;/strong&gt;，因为 iOS 会自己按系统的形状裁圆角，自带圆角反而会在四个角留下底色。&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;怎么生成的&lt;/strong&gt;：先手写 SVG（一个圆角方块加两个矩形，刻度是四条短线），再用无头浏览器把 SVG 渲染成各尺寸的 PNG，最后用 Python 的 Pillow 把 16/32/48 三张合成一个 &lt;code&gt;.ico&lt;/code&gt;。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;配置&lt;/strong&gt;：在 &lt;code&gt;src/config/siteConfig.ts&lt;/code&gt; 的 &lt;code&gt;favicon&lt;/code&gt; 里列出这三个文件；&lt;code&gt;navbar.logo&lt;/code&gt; 从图标库的 &lt;code&gt;icon&lt;/code&gt; 类型改成 &lt;code&gt;image&lt;/code&gt; 类型，指向 &lt;code&gt;/favicon.svg&lt;/code&gt;。都是主题提供的配置项，不用改源码。&lt;/li&gt;
&lt;/ul&gt;&lt;/section&gt;&lt;section&gt;&lt;h3&gt;加粗紧挨中文标点时不生效&lt;a href=&quot;#加粗紧挨中文标点时不生效&quot;&gt;&lt;span&gt;#&lt;/span&gt;&lt;/a&gt;&lt;/h3&gt;&lt;p&gt;写文章时发现，有些加粗在页面上原样显示成了星号，比如 &lt;code&gt;**OCLP 版本：2.5.1。**下载时……&lt;/code&gt;、&lt;code&gt;是**「需要时启用」**开着&lt;/code&gt;。&lt;/p&gt;&lt;p&gt;这不是主题的问题，而是 Markdown 规范本身的规定。CommonMark 判断一对 &lt;code&gt;**&lt;/code&gt; 能不能算加粗，要看它两边的字符：开头的 &lt;code&gt;**&lt;/code&gt; 如果后面紧跟标点，那它前面必须是空格或标点；结尾的 &lt;code&gt;**&lt;/code&gt; 如果前面紧挨标点，那它后面也必须是空格或标点。这套规则是照着英文设计的，英文单词之间有空格，几乎不会撞上；中文不用空格，&lt;code&gt;。**下&lt;/code&gt; 这种「标点 + 星号 + 汉字」的组合就被判成了普通文字。&lt;/p&gt;&lt;p&gt;解决办法是装 &lt;a href=&quot;https://github.com/tats-u/markdown-cjk-friendly&quot; target=&quot;_blank&quot;&gt;remark-cjk-friendly&lt;/a&gt; 插件，它专门修正中日韩文字的这条规则，放在 &lt;code&gt;astro.config.mjs&lt;/code&gt; 的 &lt;code&gt;remarkPlugins&lt;/code&gt; 第一个：&lt;/p&gt;&lt;div&gt;&lt;figure&gt;&lt;figcaption&gt;&lt;span&gt;&lt;/span&gt;&lt;span&gt;Terminal window&lt;/span&gt;&lt;/figcaption&gt;&lt;pre&gt;&lt;code&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;1&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;pnpm&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;add&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;remark-cjk-friendly&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;/code&gt;&lt;/pre&gt;&lt;div&gt;&lt;div&gt;&lt;/div&gt;&lt;div&gt;&lt;/div&gt;&lt;/div&gt;&lt;/figure&gt;&lt;/div&gt;&lt;div&gt;&lt;figure&gt;&lt;figcaption&gt;&lt;/figcaption&gt;&lt;pre&gt;&lt;code&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;1&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;import&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;remarkCjkFriendly&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;from&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;&quot;remark-cjk-friendly&quot;&lt;/span&gt;&lt;span&gt;;&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;2&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;// ...&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;3&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;&lt;span&gt;remarkPlugins&lt;/span&gt;&lt;span&gt;: [&lt;/span&gt;&lt;span&gt;remarkCjkFriendly&lt;/span&gt;&lt;span&gt;, &lt;/span&gt;&lt;/span&gt;&lt;span&gt;/* 其他插件 */&lt;/span&gt;&lt;span&gt;],&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;/code&gt;&lt;/pre&gt;&lt;div&gt;&lt;div&gt;&lt;/div&gt;&lt;div&gt;&lt;/div&gt;&lt;/div&gt;&lt;/figure&gt;&lt;/div&gt;&lt;p&gt;装之前也可以绕过：在星号外侧加个空格，或者把标点挪到加粗外面。但一来容易忘，二来 Typora、Obsidian 预览时是正常的，只有发布后才发现，所以还是在构建这一层统一解决。&lt;/p&gt;&lt;/section&gt;&lt;section&gt;&lt;h3&gt;把对主题的修改记成清单&lt;a href=&quot;#把对主题的修改记成清单&quot;&gt;&lt;span&gt;#&lt;/span&gt;&lt;/a&gt;&lt;/h3&gt;&lt;p&gt;上面这些修复，有几处是主题没有提供配置、只能改源码的。这个仓库的主题代码是复制进来的，不是从主题仓库 fork 的，以后升级主题要手动合并，改过的文件可能和新版本冲突。&lt;/p&gt;&lt;p&gt;所以在仓库根目录加了一份 &lt;code&gt;CUSTOMIZATIONS.md&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;code&gt;astro.config.mjs&lt;/code&gt;（注入自己的两个脚本：刷新后接着放音乐、欢迎页的挂载和卸载）、&lt;code&gt;layout-utils.ts&lt;/code&gt; / &lt;code&gt;navigation-utils.ts&lt;/code&gt; / &lt;code&gt;swup-transitions.ts&lt;/code&gt;（分页算首页）、&lt;code&gt;SharePoster.svelte&lt;/code&gt; / &lt;code&gt;License.astro&lt;/code&gt;（分享用当前域名）、&lt;code&gt;new-post.js&lt;/code&gt;（slug 和时区）&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;配置文件&lt;/td&gt;&lt;td&gt;&lt;code&gt;src/config/&lt;/code&gt; 下的站点信息、导航、头像、壁纸、侧边栏、音乐播放器（换成自己的网易云歌单）&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;自己加的文件&lt;/td&gt;&lt;td&gt;欢迎页（套在主题的 &lt;code&gt;MainGridLayout&lt;/code&gt; 里）、&lt;code&gt;music-resume.ts&lt;/code&gt;、旧链接跳转页、ESA 配置、检查脚本（升级不会冲突，但欢迎页依赖主题的页面框架和 &lt;code&gt;#swup-container&lt;/code&gt;，升级后要看一眼）&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;删掉的文件&lt;/td&gt;&lt;td&gt;主题仓库自己的 CI、Issue 模板、Dependabot、Vercel / Cloudflare 配置&lt;/td&gt;&lt;/tr&gt;&lt;/tbody&gt;&lt;/table&gt;&lt;p&gt;每一项都写了改了什么、为什么改、升级后怎么检查，最后附上升级步骤，以及当前所基于的主题版本（Firefly v6.16.8）。规则是：以后每改一次主题源码，都要同步更新这份清单。&lt;/p&gt;&lt;p&gt;网址规则这类问题，自动检查能兜底：网址变了构建就会失败。但残影、横幅、分享链接这些是显示上的问题，检查脚本发现不了，只能靠清单里的”升级后怎么检查”逐项人工看一遍。&lt;/p&gt;&lt;/section&gt;&lt;section&gt;&lt;h3&gt;主题和 Astro 要不要经常升级&lt;a href=&quot;#主题和-astro-要不要经常升级&quot;&gt;&lt;span&gt;#&lt;/span&gt;&lt;/a&gt;&lt;/h3&gt;&lt;p&gt;Astro 更新很勤：大版本大约一年一次（会有不兼容的改动），小版本大约一个月一次，补丁几乎每周都有。但个人博客不需要追：&lt;/p&gt;&lt;ul&gt;
&lt;li&gt;线上只是构建好的静态文件，没有服务器程序，Astro 的漏洞一般影响不到已经发布的网站；&lt;/li&gt;
&lt;li&gt;Astro 只在构建时运行，跑完就结束，不对外提供服务；&lt;/li&gt;
&lt;li&gt;Firefly 的 &lt;code&gt;package.json&lt;/code&gt; 里把 Astro 锁在一个精确版本，主题作者是基于这个版本测试的，单独升级 Astro 反而可能让主题出问题。&lt;/li&gt;
&lt;/ul&gt;&lt;p&gt;所以策略是&lt;strong&gt;不单独升级 Astro，只跟着主题升级&lt;/strong&gt;，几个月看一次 Firefly 有没有新版本，有需要的功能再升；遇到严重的安全公告再单独处理。删掉 Dependabot 也是这个考虑：它会每周自动提交依赖升级，对博客来说是干扰。&lt;/p&gt;&lt;p&gt;另外还定了一条规矩：&lt;strong&gt;以后只要改了博客的设定，都同步更新这篇文章&lt;/strong&gt;，和 &lt;code&gt;CUSTOMIZATIONS.md&lt;/code&gt; 一起，作为这个博客的”施工记录”。&lt;/p&gt;&lt;/section&gt;&lt;/section&gt;
&lt;section&gt;&lt;h2&gt;十三、写作工具配置&lt;a href=&quot;#十三写作工具配置&quot;&gt;&lt;span&gt;#&lt;/span&gt;&lt;/a&gt;&lt;/h2&gt;&lt;p&gt;图片跟着文章走的前提是：&lt;strong&gt;编辑器粘贴图片时，要自动存到 &lt;code&gt;文章名.assets/&lt;/code&gt; 文件夹，并写成相对路径&lt;/strong&gt;。否则图片会落到别的地方，或者变成本机绝对路径，换台电脑就打不开了。&lt;/p&gt;&lt;section&gt;&lt;h3&gt;Typora&lt;a href=&quot;#typora&quot;&gt;&lt;span&gt;#&lt;/span&gt;&lt;/a&gt;&lt;/h3&gt;&lt;p&gt;偏好设置（&lt;code&gt;Cmd + ,&lt;/code&gt;）→ &lt;strong&gt;图像&lt;/strong&gt;：&lt;/p&gt;&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;插入图片时&lt;/strong&gt;：下拉框里直接选 Typora 自带的 **复制图片到 ./&lt;span&gt;&lt;span&gt;filename.assets文件夹∗∗。‘{filename}.assets 文件夹**。`&lt;/span&gt;&lt;span&gt;&lt;span&gt;&lt;span&gt;&lt;/span&gt;&lt;span&gt;&lt;span&gt;f&lt;/span&gt;&lt;span&gt;i&lt;/span&gt;&lt;span&gt;l&lt;/span&gt;&lt;span&gt;e&lt;/span&gt;&lt;span&gt;nam&lt;/span&gt;&lt;span&gt;e&lt;/span&gt;&lt;/span&gt;&lt;span&gt;.&lt;/span&gt;&lt;span&gt;a&lt;/span&gt;&lt;span&gt;sse&lt;/span&gt;&lt;span&gt;t&lt;/span&gt;&lt;span&gt;s&lt;/span&gt;&lt;span&gt;文件夹&lt;/span&gt;&lt;span&gt;&lt;/span&gt;&lt;span&gt;∗&lt;/span&gt;&lt;span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span&gt;&lt;span&gt;&lt;/span&gt;&lt;span&gt;∗&lt;/span&gt;&lt;span&gt;。&lt;/span&gt;&lt;span&gt;‘&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;{filename}&lt;code&gt;是 Typora 的变量，代表当前文件名（不带&lt;/code&gt;.md`）。&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;为相对路径添加 &lt;code&gt;./&lt;/code&gt;&lt;/strong&gt;：和现有文章的写法 &lt;code&gt;./xxx.assets/xxx.png&lt;/code&gt; 保持一致。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;插入时自动转义图片 URL&lt;/strong&gt;：不勾。勾了会把中文文件名转成 &lt;code&gt;%E4%BB%8E...&lt;/code&gt; 这样的编码，不方便阅读，Astro 不需要它。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;允许根据 YAML 设置自动上传图片&lt;/strong&gt;：不勾。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;上传服务&lt;/strong&gt;：选”无”。不再用 PicGo 上传图床。&lt;/li&gt;
&lt;/ul&gt;&lt;p&gt;配置好后，粘贴一张截图，Typora 会生成 &lt;code&gt;![](./文章名.assets/image-20261023103000000.png)&lt;/code&gt;，文件夹不存在会自动创建。&lt;/p&gt;&lt;blockquote&gt;&lt;p&gt;注意：先保存文件再粘贴图片。新建的文件还没有文件名，Typora 不知道该把图片放到哪里。&lt;/p&gt;&lt;/blockquote&gt;&lt;/section&gt;&lt;section&gt;&lt;h3&gt;Obsidian&lt;a href=&quot;#obsidian&quot;&gt;&lt;span&gt;#&lt;/span&gt;&lt;/a&gt;&lt;/h3&gt;&lt;p&gt;&lt;strong&gt;库（vault）只打开 &lt;code&gt;src/content/posts&lt;/code&gt;&lt;/strong&gt;，不要打开整个项目，否则它会去索引 &lt;code&gt;node_modules&lt;/code&gt; 里几万个文件。Obsidian 会在这里生成 &lt;code&gt;.obsidian/&lt;/code&gt; 配置文件夹，已经写进 &lt;code&gt;.gitignore&lt;/code&gt;，不会被提交。&lt;/p&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;使用 &lt;a href=&quot;/posts/Wiki%20%E9%93%BE%E6%8E%A5/&quot;&gt;Wiki 链接&lt;/a&gt;&lt;/strong&gt;：关闭。Astro 不认识 &lt;code&gt;[[ ]]&lt;/code&gt; 和 &lt;code&gt;![[图片]]&lt;/code&gt;，必须是标准 Markdown 链接。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;自动更新内部链接&lt;/strong&gt;：打开。在 Obsidian 里改文件名、移动文件时，引用它的链接会跟着改。&lt;/li&gt;
&lt;/ul&gt;&lt;p&gt;&lt;strong&gt;图片位置&lt;/strong&gt;：Obsidian 自带的”附件默认存放路径”只能选一个固定名字的子文件夹（比如统一叫 &lt;code&gt;attachments&lt;/code&gt;），做不到”每篇文章一个同名 &lt;code&gt;.assets&lt;/code&gt;”。需要装社区插件 &lt;strong&gt;Custom Attachment Location&lt;/strong&gt;：&lt;/p&gt;&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;附件位置&lt;/strong&gt;：&lt;code&gt;./${noteFileName}.assets&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;附件文件名&lt;/strong&gt;：&lt;code&gt;${date:YYYYMMDDHHmmssSSS}&lt;/code&gt;，用时间戳命名。Obsidian 默认的 &lt;code&gt;Pasted image 2026...png&lt;/code&gt; 带空格，放在 Markdown 链接里容易出问题。&lt;/li&gt;
&lt;li&gt;插件的变量写法可能随版本变化，以插件设置页里的说明为准。&lt;/li&gt;
&lt;/ul&gt;&lt;p&gt;&lt;strong&gt;文章模板&lt;/strong&gt;：用 Obsidian 自带的”模板”核心插件，存一份开头的元信息，新建文章时插入。注意 &lt;code&gt;published&lt;/code&gt; 要写成带时区的格式，比如 &lt;code&gt;2026-10-23T10:30:00+08:00&lt;/code&gt;。Obsidian 属性面板里的日期默认不带时区，会被当成 UTC，显示的时间差 8 小时。&lt;/p&gt;&lt;p&gt;两个编辑器的约定一致，同一篇文章用 Typora 写还是用 Obsidian 写都可以，图片位置和链接写法都一样。&lt;/p&gt;&lt;/section&gt;&lt;/section&gt;
&lt;section&gt;&lt;h2&gt;十四、现在的发布流程&lt;a href=&quot;#十四现在的发布流程&quot;&gt;&lt;span&gt;#&lt;/span&gt;&lt;/a&gt;&lt;/h2&gt;&lt;div&gt;&lt;figure&gt;&lt;figcaption&gt;&lt;/figcaption&gt;&lt;pre&gt;&lt;code&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;1&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;本地写文章 → git push 到 master&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;2&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;&lt;span&gt;                 &lt;/span&gt;&lt;/span&gt;&lt;span&gt;│&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;3&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;&lt;span&gt;                 &lt;/span&gt;&lt;/span&gt;&lt;span&gt;├──► GitHub Actions：构建 → 检查 → 发布到 tracerzzz.github.io&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;4&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;&lt;span&gt;                 &lt;/span&gt;&lt;/span&gt;&lt;span&gt;│&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;5&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;&lt;span&gt;                 &lt;/span&gt;&lt;/span&gt;&lt;span&gt;└──► ESA Pages：拉取 → 构建 → 发布到 tracerzzz.cn&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;/code&gt;&lt;/pre&gt;&lt;div&gt;&lt;div&gt;&lt;/div&gt;&lt;div&gt;&lt;/div&gt;&lt;/div&gt;&lt;/figure&gt;&lt;/div&gt;&lt;p&gt;日常写作就是：&lt;/p&gt;&lt;div&gt;&lt;figure&gt;&lt;figcaption&gt;&lt;span&gt;&lt;/span&gt;&lt;span&gt;Terminal window&lt;/span&gt;&lt;/figcaption&gt;&lt;pre&gt;&lt;code&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;1&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;pnpm&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;new-post&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;git/新文章标题&lt;/span&gt;&lt;span&gt;   &lt;/span&gt;&lt;span&gt;# 生成带元信息的新文件&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;2&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;pnpm&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;dev&lt;/span&gt;&lt;span&gt;                       &lt;/span&gt;&lt;span&gt;# 边写边预览&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;3&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;git&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;add&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;-A&lt;/span&gt;&lt;span&gt; &amp;amp;&amp;amp; &lt;/span&gt;&lt;span&gt;git&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;commit&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;-m&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;&quot;新文章&quot;&lt;/span&gt;&lt;span&gt; &amp;amp;&amp;amp; &lt;/span&gt;&lt;span&gt;git&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;push&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;/code&gt;&lt;/pre&gt;&lt;div&gt;&lt;div&gt;&lt;/div&gt;&lt;div&gt;&lt;/div&gt;&lt;/div&gt;&lt;/figure&gt;&lt;/div&gt;&lt;p&gt;两三分钟后，两个站点都更新了。构建失败不会替换线上版本；ESA 的部署记录里也可以一键回滚到之前的版本。&lt;/p&gt;&lt;section&gt;&lt;h3&gt;换一台电脑写博客&lt;a href=&quot;#换一台电脑写博客&quot;&gt;&lt;span&gt;#&lt;/span&gt;&lt;/a&gt;&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;/tr&gt;&lt;/thead&gt;&lt;tbody&gt;&lt;tr&gt;&lt;td&gt;Git&lt;/td&gt;&lt;td&gt;任意新版本，macOS 自带&lt;/td&gt;&lt;td&gt;拉代码、提交、推送&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;Node.js&lt;/td&gt;&lt;td&gt;22.23 以上（&lt;code&gt;package.json&lt;/code&gt; 里的 &lt;code&gt;engines&lt;/code&gt;）&lt;/td&gt;&lt;td&gt;跑 Astro&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;pnpm&lt;/td&gt;&lt;td&gt;11.22.0（&lt;code&gt;package.json&lt;/code&gt; 里的 &lt;code&gt;packageManager&lt;/code&gt;）&lt;/td&gt;&lt;td&gt;装依赖、预览、构建&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;GitHub 登录&lt;/td&gt;&lt;td&gt;个人访问令牌，或 &lt;code&gt;gh auth login&lt;/code&gt;&lt;/td&gt;&lt;td&gt;推送&lt;/td&gt;&lt;/tr&gt;&lt;/tbody&gt;&lt;/table&gt;&lt;div&gt;&lt;figure&gt;&lt;figcaption&gt;&lt;span&gt;&lt;/span&gt;&lt;span&gt;Terminal window&lt;/span&gt;&lt;/figcaption&gt;&lt;pre&gt;&lt;code&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;1&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;corepack&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;enable&lt;/span&gt;&lt;span&gt;                                          &lt;/span&gt;&lt;span&gt;# Node 自带，按 packageManager 自动用对的 pnpm 版本&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;2&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;npm&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;config&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;set&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;registry&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;https://registry.npmmirror.com&lt;/span&gt;&lt;span&gt;   &lt;/span&gt;&lt;span&gt;# 国内 npm 镜像&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;3&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;mkdir&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;-p&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;~/blog&lt;/span&gt;&lt;span&gt; &amp;amp;&amp;amp; &lt;/span&gt;&lt;span&gt;cd&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;~/blog&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;4&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;git&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;clone&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;https://github.com/tracerZzz/tracerZzz.github.io.git&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;astro&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;5&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;cd&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;astro&lt;/span&gt;&lt;span&gt; &amp;amp;&amp;amp; &lt;/span&gt;&lt;span&gt;pnpm&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;install&lt;/span&gt;&lt;span&gt; &amp;amp;&amp;amp; &lt;/span&gt;&lt;span&gt;pnpm&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;dev&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;6&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;git&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;config&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;--global&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;user.name&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;&quot;tracerZzz&quot;&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;7&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;git&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;config&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;--global&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;user.email&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;&quot;tracerZzz@users.noreply.github.com&quot;&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;/code&gt;&lt;/pre&gt;&lt;div&gt;&lt;div&gt;&lt;/div&gt;&lt;div&gt;&lt;/div&gt;&lt;/div&gt;&lt;/figure&gt;&lt;/div&gt;&lt;p&gt;用令牌推送时，第一次 &lt;code&gt;git push&lt;/code&gt; 输入用户名和令牌（不是登录密码），macOS 自带的 git 会把它存进钥匙串，以后不用再输。&lt;/p&gt;&lt;p&gt;在一台&lt;a href=&quot;/posts/oclp-sonoma-2016-macbook-pro/&quot;&gt;用 OCLP 升到 Sonoma 的 2016 款 MacBook Pro&lt;/a&gt; 上搭这套环境时，遇到两个坑：&lt;/p&gt;&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Homebrew 装 Node 要现场编译几个小时&lt;/strong&gt;：&lt;code&gt;brew install node@22&lt;/code&gt; 之后开始一个个编译 openssl、python、cmake。Homebrew 已经不再为 Intel 芯片 + Sonoma 这种组合提供编译好的安装包（bottle），只能从源码编。苹果芯片的 Mac 不会这样。解决：Node 不走 Homebrew，用官方 &lt;code&gt;.pkg&lt;/code&gt; 安装包（国内可以从 npmmirror 镜像的 &lt;code&gt;latest-v22.x&lt;/code&gt; 目录下载）或者 nvm，几分钟就好。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Homebrew 是另一个用户装的&lt;/strong&gt;：Intel Mac 上 Homebrew 装在 &lt;code&gt;/usr/local&lt;/code&gt;，所有者是当初安装它的用户，换了用户就没有写入权限。用 &lt;code&gt;sudo chown -R $(whoami):admin $(brew --prefix)/*&lt;/code&gt; 改一下所有权。&lt;/li&gt;
&lt;/ul&gt;&lt;/section&gt;&lt;/section&gt;
&lt;section&gt;&lt;h2&gt;十五、还没做的&lt;a href=&quot;#十五还没做的&quot;&gt;&lt;span&gt;#&lt;/span&gt;&lt;/a&gt;&lt;/h2&gt;&lt;ul&gt;
&lt;li&gt;正式切换到 Obsidian 管理所有 Markdown。&lt;/li&gt;
&lt;li&gt;草稿慢慢整理发布。&lt;/li&gt;
&lt;/ul&gt;&lt;p&gt;原本还列了两项，后来决定不做：&lt;code&gt;www&lt;/code&gt; 跳转到根域名（&lt;code&gt;www&lt;/code&gt; 已经在 ESA 上绑定，能直接访问，canonical 指向根域名，不需要再跳）；访问统计（ESA 控制台自带基本的访问数据，够用了，不再额外接第三方统计脚本）。&lt;/p&gt;&lt;/section&gt;
&lt;section&gt;&lt;h2&gt;回头看&lt;a href=&quot;#回头看&quot;&gt;&lt;span&gt;#&lt;/span&gt;&lt;/a&gt;&lt;/h2&gt;&lt;p&gt;这次迁移最大的收获，是把”文章依赖外部服务”变成了”所有东西都在一个仓库里”：文章、图片、欢迎页、构建配置、部署配置，全部跟着 git 走。图床过期、平台变动、换主题、换编辑器，都不会再让博客坏掉。后来真的换了一次主题，从开分支试用到正式上线只用了一个下午，所有网址一个没变，算是验证了这个结构。&lt;/p&gt;&lt;p&gt;另一个体会是：&lt;strong&gt;大部分问题都出在”两个东西之间的约定”上&lt;/strong&gt;。比如空间名和 bucket 名不是一回事，BSD sed 和 GNU sed 行为不同，bash 3.2 不能按字节处理 UTF-8，Jekyll 会去解析 &lt;code&gt;.astro&lt;/code&gt; 文件，泛域名证书不包含根域名。每个问题单独看都不难，难的是知道该往哪个方向怀疑。&lt;/p&gt;&lt;/section&gt;</content:encoded></item><item><title>每小时 10T 日志里找热点：用 Spark 和小根堆计算 CDN 的 TopN</title><link>https://tracerzzz.cn/posts/spark-cdn-topn-min-heap/</link><guid isPermaLink="true">https://tracerzzz.cn/posts/spark-cdn-topn-min-heap/</guid><description>在百度云 CDN 时，我负责用 Spark 从访问日志里统计热点：每个域名、每个用户、全局的 Top URL、Top Referer 等，按请求数和流量排序，小时级日志最多 10T。这篇讲为什么不能直接全量排序，小根堆怎么把内存控制在 N 个元素，MapReduce 阶段怎么合并，以及多层级统计时怎么复用下层结果。</description><pubDate>Mon, 15 Jun 2020 12:00:00 GMT</pubDate><content:encoded>&lt;p&gt;在百度云 CDN 时，我做过一个热点分析的任务：从 CDN 节点的 nginx 访问日志里，统计出各个维度的热门资源，比如每个域名访问最多的 URL、带来流量最多的 URL、主要的 Referer 和 User-Agent。这类热点数据通常用于控制台展示、排查热点、缓存预热和发现盗链。&lt;/p&gt;
&lt;p&gt;日志量很大，小时级最高能到 10T。这篇讲这个任务的核心思路：&lt;strong&gt;用小根堆在 MapReduce 里算 TopN&lt;/strong&gt;。&lt;/p&gt;
&lt;blockquote&gt;&lt;p&gt;说明：这篇是 2026 年根据当年在百度做的项目回忆整理的，发布时间标在项目所在的时间段（2019–2021 年）。时间久远，当时的具体代码和参数已经记不清，下面的代码是按通用做法整理的 PySpark 示例，用来说明思路，不是原始实现。&lt;/p&gt;&lt;/blockquote&gt;
&lt;section&gt;&lt;h2&gt;一、要统计什么&lt;a href=&quot;#一要统计什么&quot;&gt;&lt;span&gt;#&lt;/span&gt;&lt;/a&gt;&lt;/h2&gt;&lt;p&gt;统计分三个层级，从小到大：&lt;/p&gt;&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;域名级&lt;/strong&gt;：每个加速域名下的 TopN；&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;用户级&lt;/strong&gt;：一个客户名下所有域名合在一起的 TopN；&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;全局&lt;/strong&gt;：整个平台的 TopN。&lt;/li&gt;
&lt;/ul&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;URL&lt;/td&gt;&lt;td&gt;请求数（PV）、流量&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;Referer&lt;/td&gt;&lt;td&gt;请求数&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;User-Agent&lt;/td&gt;&lt;td&gt;请求数&lt;/td&gt;&lt;/tr&gt;&lt;/tbody&gt;&lt;/table&gt;&lt;p&gt;这和主流 CDN 厂商控制台提供的 TOP 统计基本一致。比如百度智能云 CDN 的文档里就列了 TOP URL、TOP Referer、TOP HTTP 状态码、TOP 域名，统计范围是「当前域名或当前用户」。&lt;/p&gt;&lt;/section&gt;
&lt;section&gt;&lt;h2&gt;二、为什么不能直接全量排序&lt;a href=&quot;#二为什么不能直接全量排序&quot;&gt;&lt;span&gt;#&lt;/span&gt;&lt;/a&gt;&lt;/h2&gt;&lt;p&gt;最直接的写法是：按 (域名, URL) 分组求和，然后按值排序取前 N 个。&lt;/p&gt;&lt;div&gt;&lt;figure&gt;&lt;figcaption&gt;&lt;/figcaption&gt;&lt;pre&gt;&lt;code&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;1&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;&lt;span&gt;totals &lt;/span&gt;&lt;span&gt;=&lt;/span&gt;&lt;span&gt; logs.&lt;/span&gt;&lt;span&gt;map&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;/span&gt;&lt;span&gt;lambda&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;r&lt;/span&gt;&lt;span&gt;&lt;span&gt;: ((r.domain, r.url), r.bytes)).&lt;/span&gt;&lt;span&gt;reduceByKey&lt;/span&gt;&lt;span&gt;(add)&lt;/span&gt;&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;2&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;&lt;span&gt;top &lt;/span&gt;&lt;span&gt;=&lt;/span&gt;&lt;span&gt; totals.&lt;/span&gt;&lt;span&gt;sortBy&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;/span&gt;&lt;span&gt;lambda&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;kv&lt;/span&gt;&lt;span&gt;&lt;span&gt;: &lt;/span&gt;&lt;span&gt;-&lt;/span&gt;&lt;span&gt;kv[&lt;/span&gt;&lt;/span&gt;&lt;span&gt;1&lt;/span&gt;&lt;span&gt;&lt;span&gt;]).&lt;/span&gt;&lt;span&gt;take&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;/span&gt;&lt;span&gt;100&lt;/span&gt;&lt;span&gt;)&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;/code&gt;&lt;/pre&gt;&lt;div&gt;&lt;div&gt;&lt;/div&gt;&lt;div&gt;&lt;/div&gt;&lt;/div&gt;&lt;/figure&gt;&lt;/div&gt;&lt;p&gt;问题在于规模：&lt;/p&gt;&lt;ul&gt;
&lt;li&gt;一小时的日志里，不同的 URL 数量是亿级的，带参数的动态 URL 更多；&lt;/li&gt;
&lt;li&gt;&lt;code&gt;sortBy&lt;/code&gt; 是&lt;strong&gt;全量排序&lt;/strong&gt;，所有数据都要按值重新分区（shuffle），代价和数据量成正比；&lt;/li&gt;
&lt;li&gt;我们只要每个域名的前 N 个，比如前 100 个，却把上亿条数据完整地排了一遍，绝大部分排序工作都是浪费的。&lt;/li&gt;
&lt;/ul&gt;&lt;p&gt;而且这还只是全局的一个 TopN。要按域名分别取 TopN，就得 &lt;code&gt;groupByKey&lt;/code&gt; 把每个域名的所有 URL 拉到一起再排序，大域名的数据会全部挤在一个 executor 上，很容易内存溢出。&lt;/p&gt;&lt;/section&gt;
&lt;section&gt;&lt;h2&gt;三、小根堆：只记住前 N 名&lt;a href=&quot;#三小根堆只记住前-n-名&quot;&gt;&lt;span&gt;#&lt;/span&gt;&lt;/a&gt;&lt;/h2&gt;&lt;p&gt;取 TopN 有一个经典的做法：&lt;strong&gt;维护一个大小为 N 的小根堆&lt;/strong&gt;。&lt;/p&gt;&lt;ul&gt;
&lt;li&gt;小根堆的堆顶，永远是堆里&lt;strong&gt;最小&lt;/strong&gt;的元素，也就是「当前第 N 名」；&lt;/li&gt;
&lt;li&gt;来一个新元素，如果堆还没满，直接放进去；&lt;/li&gt;
&lt;li&gt;如果堆满了，就和堆顶比：比堆顶大，说明它能挤进前 N 名，替换掉堆顶；比堆顶小，直接丢弃。&lt;/li&gt;
&lt;/ul&gt;&lt;div&gt;&lt;figure&gt;&lt;figcaption&gt;&lt;/figcaption&gt;&lt;pre&gt;&lt;code&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;1&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;import&lt;/span&gt;&lt;span&gt; heapq&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;2&lt;/div&gt;&lt;/div&gt;&lt;div&gt;
&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;3&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;&lt;span&gt;N &lt;/span&gt;&lt;span&gt;=&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;/span&gt;&lt;span&gt;100&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;4&lt;/div&gt;&lt;/div&gt;&lt;div&gt;
&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;5&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;def&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;push&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;heap&lt;/span&gt;&lt;span&gt;,&lt;/span&gt;&lt;span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;item&lt;/span&gt;&lt;/span&gt;&lt;span&gt;):&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;6&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;    &lt;/span&gt;&lt;span&gt;&quot;&quot;&quot;item = (指标值, URL)；heap 最多保留 N 个最大的元素&quot;&quot;&quot;&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;7&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;    &lt;/span&gt;&lt;span&gt;if&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;len&lt;/span&gt;&lt;span&gt;&lt;span&gt;(heap) &lt;/span&gt;&lt;span&gt;&amp;lt;&lt;/span&gt;&lt;span&gt; N:&lt;/span&gt;&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;8&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;&lt;span&gt;        &lt;/span&gt;&lt;/span&gt;&lt;span&gt;heapq.&lt;/span&gt;&lt;span&gt;heappush&lt;/span&gt;&lt;span&gt;(heap, item)&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;9&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;    &lt;/span&gt;&lt;span&gt;elif&lt;/span&gt;&lt;span&gt;&lt;span&gt; item &lt;/span&gt;&lt;span&gt;&amp;gt;&lt;/span&gt;&lt;span&gt; heap[&lt;/span&gt;&lt;/span&gt;&lt;span&gt;0&lt;/span&gt;&lt;span&gt;]:            &lt;/span&gt;&lt;span&gt;# 比当前第 N 名大&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;10&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;&lt;span&gt;        &lt;/span&gt;&lt;/span&gt;&lt;span&gt;heapq.&lt;/span&gt;&lt;span&gt;heapreplace&lt;/span&gt;&lt;span&gt;(heap, item)&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;11&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;    &lt;/span&gt;&lt;span&gt;return&lt;/span&gt;&lt;span&gt; heap&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;/code&gt;&lt;/pre&gt;&lt;div&gt;&lt;div&gt;&lt;/div&gt;&lt;div&gt;&lt;/div&gt;&lt;/div&gt;&lt;/figure&gt;&lt;/div&gt;&lt;p&gt;为什么用&lt;strong&gt;小根堆&lt;/strong&gt;取&lt;strong&gt;最大的&lt;/strong&gt; N 个，而不是大根堆？因为我们需要随时知道「门槛」是多少，也就是前 N 名里最小的那个。小根堆的堆顶正好就是这个门槛，比较一次就能决定新元素要不要进来。&lt;/p&gt;&lt;p&gt;它的好处：&lt;/p&gt;&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;内存固定&lt;/strong&gt;：不管输入多少条，堆里永远只有 N 个元素；&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;时间省&lt;/strong&gt;：每个元素最多一次堆操作，复杂度 O(M log N)。M 是元素总数，N 是 100 这样的小数，log N 几乎是常数；而全量排序是 O(M log M)。&lt;/li&gt;
&lt;/ul&gt;&lt;/section&gt;
&lt;section&gt;&lt;h2&gt;四、放进 MapReduce：先局部取 TopN，再合并&lt;a href=&quot;#四放进-mapreduce先局部取-topn再合并&quot;&gt;&lt;span&gt;#&lt;/span&gt;&lt;/a&gt;&lt;/h2&gt;&lt;p&gt;先看整体流程，后面两节逐步展开：&lt;/p&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;&lt;/span&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;&lt;/span&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;&lt;/span&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;&lt;/span&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;&lt;/span&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;&lt;/span&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;&lt;/span&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;&lt;/span&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;&lt;/span&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;&lt;/span&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;&lt;/span&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;&lt;/span&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;&lt;/span&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;&lt;p&gt;按用户合并&lt;/p&gt;&lt;/span&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;&lt;p&gt;全部合并&lt;/p&gt;&lt;/span&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;&lt;p&gt;Referer、UA 跨域名，每层单独求和&lt;/p&gt;&lt;/span&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;&lt;p&gt;nginx 访问日志&lt;br /&gt;&lt;br /&gt;小时级最高 10T&lt;/p&gt;&lt;/span&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;&lt;p&gt;分区 1&lt;/p&gt;&lt;/span&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;&lt;p&gt;分区 2&lt;/p&gt;&lt;/span&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;&lt;p&gt;分区 …&lt;/p&gt;&lt;/span&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;&lt;p&gt;① reduceByKey&lt;br /&gt;&lt;br /&gt;按（域名, URL）求和&lt;br /&gt;&lt;br /&gt;Map 端先局部求和再 shuffle&lt;/p&gt;&lt;/span&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;&lt;p&gt;分区 1&lt;br /&gt;&lt;br /&gt;每个域名一个小根堆&lt;br /&gt;&lt;br /&gt;只留前 N&lt;/p&gt;&lt;/span&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;&lt;p&gt;分区 2&lt;br /&gt;&lt;br /&gt;每个域名一个小根堆&lt;br /&gt;&lt;br /&gt;只留前 N&lt;/p&gt;&lt;/span&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;&lt;p&gt;分区 …&lt;br /&gt;&lt;br /&gt;每个域名一个小根堆&lt;br /&gt;&lt;br /&gt;只留前 N&lt;/p&gt;&lt;/span&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;&lt;p&gt;② aggregateByKey 合并&lt;br /&gt;&lt;br /&gt;同一域名的各分区堆合并&lt;br /&gt;&lt;br /&gt;仍只留 N 个&lt;/p&gt;&lt;/span&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;&lt;p&gt;域名级 TopN&lt;br /&gt;&lt;br /&gt;（域名数 × N 条）&lt;/p&gt;&lt;/span&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;&lt;p&gt;用户级 TopN&lt;/p&gt;&lt;/span&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;&lt;p&gt;全局 TopN&lt;/p&gt;&lt;/span&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;&lt;p&gt;各层级的&lt;br /&gt;&lt;br /&gt;Top Referer / Top UA&lt;/p&gt;&lt;/span&gt;&lt;/div&gt;
&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;&lt;/span&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;&lt;/span&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;&lt;/span&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;&lt;/span&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;&lt;/span&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;&lt;/span&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;&lt;/span&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;&lt;/span&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;&lt;/span&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;&lt;/span&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;&lt;/span&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;&lt;/span&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;&lt;/span&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;&lt;p&gt;按用户合并&lt;/p&gt;&lt;/span&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;&lt;p&gt;全部合并&lt;/p&gt;&lt;/span&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;&lt;p&gt;Referer、UA 跨域名，每层单独求和&lt;/p&gt;&lt;/span&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;&lt;p&gt;nginx 访问日志&lt;br /&gt;&lt;br /&gt;小时级最高 10T&lt;/p&gt;&lt;/span&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;&lt;p&gt;分区 1&lt;/p&gt;&lt;/span&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;&lt;p&gt;分区 2&lt;/p&gt;&lt;/span&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;&lt;p&gt;分区 …&lt;/p&gt;&lt;/span&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;&lt;p&gt;① reduceByKey&lt;br /&gt;&lt;br /&gt;按（域名, URL）求和&lt;br /&gt;&lt;br /&gt;Map 端先局部求和再 shuffle&lt;/p&gt;&lt;/span&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;&lt;p&gt;分区 1&lt;br /&gt;&lt;br /&gt;每个域名一个小根堆&lt;br /&gt;&lt;br /&gt;只留前 N&lt;/p&gt;&lt;/span&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;&lt;p&gt;分区 2&lt;br /&gt;&lt;br /&gt;每个域名一个小根堆&lt;br /&gt;&lt;br /&gt;只留前 N&lt;/p&gt;&lt;/span&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;&lt;p&gt;分区 …&lt;br /&gt;&lt;br /&gt;每个域名一个小根堆&lt;br /&gt;&lt;br /&gt;只留前 N&lt;/p&gt;&lt;/span&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;&lt;p&gt;② aggregateByKey 合并&lt;br /&gt;&lt;br /&gt;同一域名的各分区堆合并&lt;br /&gt;&lt;br /&gt;仍只留 N 个&lt;/p&gt;&lt;/span&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;&lt;p&gt;域名级 TopN&lt;br /&gt;&lt;br /&gt;（域名数 × N 条）&lt;/p&gt;&lt;/span&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;&lt;p&gt;用户级 TopN&lt;/p&gt;&lt;/span&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;&lt;p&gt;全局 TopN&lt;/p&gt;&lt;/span&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;&lt;p&gt;各层级的&lt;br /&gt;&lt;br /&gt;Top Referer / Top UA&lt;/p&gt;&lt;/span&gt;&lt;/div&gt;
&lt;/div&gt;&lt;/div&gt;&lt;/div&gt;&lt;p&gt;数据量在每一步都在收缩：原始日志先被求和压缩成「每个 URL 一条」，再被小根堆压缩成「每个域名 N 条」，用户级和全局只是在这个很小的结果上再合并一次。&lt;/p&gt;&lt;p&gt;小根堆在单机上很好用。到了分布式环境，关键是一个性质：&lt;/p&gt;&lt;p&gt;&lt;strong&gt;全局的前 N 名，一定在各个分区各自的前 N 名里。&lt;/strong&gt;&lt;/p&gt;&lt;p&gt;道理很简单：如果某个元素在它所在的分区里连前 N 都排不进，说明那个分区里已经有 N 个比它大的元素了，它在全局也不可能进前 N。&lt;/p&gt;&lt;p&gt;所以计算可以分两步：&lt;/p&gt;&lt;ol&gt;
&lt;li&gt;&lt;strong&gt;Map 端&lt;/strong&gt;：每个分区各自维护一个小根堆，只保留本分区的前 N 名；&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Reduce 端&lt;/strong&gt;：把各分区的堆合并，合并时还是用同一个 &lt;code&gt;push&lt;/code&gt;，最后仍然只保留 N 个。&lt;/li&gt;
&lt;/ol&gt;&lt;p&gt;每个分区交给下游的只有 N 条数据，shuffle 的数据量从「所有 URL」收缩到了「分区数 × N」。&lt;/p&gt;&lt;p&gt;在 Spark 里，「按域名分组、每组维护一个堆」可以用 &lt;code&gt;aggregateByKey&lt;/code&gt; 实现，它天然就是这种「分区内先聚合、分区间再合并」的结构：&lt;/p&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;&lt;figure&gt;&lt;figcaption&gt;&lt;/figcaption&gt;&lt;pre&gt;&lt;code&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;1&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;from&lt;/span&gt;&lt;span&gt; operator &lt;/span&gt;&lt;span&gt;import&lt;/span&gt;&lt;span&gt; add&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;2&lt;/div&gt;&lt;/div&gt;&lt;div&gt;
&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;3&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;def&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;merge&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;heap_a&lt;/span&gt;&lt;span&gt;,&lt;/span&gt;&lt;span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;heap_b&lt;/span&gt;&lt;/span&gt;&lt;span&gt;):&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;4&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;    &lt;/span&gt;&lt;span&gt;for&lt;/span&gt;&lt;span&gt; item &lt;/span&gt;&lt;span&gt;in&lt;/span&gt;&lt;span&gt; heap_b:&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;5&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;&lt;span&gt;        &lt;/span&gt;&lt;/span&gt;&lt;span&gt;push&lt;/span&gt;&lt;span&gt;(heap_a, item)&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;6&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;    &lt;/span&gt;&lt;span&gt;return&lt;/span&gt;&lt;span&gt; heap_a&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;7&lt;/div&gt;&lt;/div&gt;&lt;div&gt;
&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;8&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;# 第一步：先把每个 (域名, URL) 在整个小时内的流量加总&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;9&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;&lt;span&gt;totals &lt;/span&gt;&lt;span&gt;=&lt;/span&gt;&lt;span&gt; (logs&lt;/span&gt;&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;10&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;&lt;span&gt;          &lt;/span&gt;&lt;/span&gt;&lt;span&gt;.&lt;/span&gt;&lt;span&gt;map&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;lambda&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;r&lt;/span&gt;&lt;span&gt;: ((r.domain, r.url), r.bytes))&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;11&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;&lt;span&gt;          &lt;/span&gt;&lt;/span&gt;&lt;span&gt;.&lt;/span&gt;&lt;span&gt;reduceByKey&lt;/span&gt;&lt;span&gt;(add))&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;12&lt;/div&gt;&lt;/div&gt;&lt;div&gt;
&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;13&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;# 第二步：按域名维护大小为 N 的小根堆&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;14&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;#   seqOp：分区内，把 (流量, URL) 放进该域名的堆&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;15&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;#   combOp：分区间，两个堆合并&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;16&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;&lt;span&gt;domain_top &lt;/span&gt;&lt;span&gt;=&lt;/span&gt;&lt;span&gt; (totals&lt;/span&gt;&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;17&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;&lt;span&gt;              &lt;/span&gt;&lt;/span&gt;&lt;span&gt;.&lt;/span&gt;&lt;span&gt;map&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;lambda&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;kv&lt;/span&gt;&lt;span&gt;: (kv[&lt;/span&gt;&lt;span&gt;0&lt;/span&gt;&lt;span&gt;][&lt;/span&gt;&lt;span&gt;0&lt;/span&gt;&lt;span&gt;], (kv[&lt;/span&gt;&lt;span&gt;1&lt;/span&gt;&lt;span&gt;], kv[&lt;/span&gt;&lt;span&gt;0&lt;/span&gt;&lt;span&gt;][&lt;/span&gt;&lt;span&gt;1&lt;/span&gt;&lt;span&gt;])))   &lt;/span&gt;&lt;span&gt;# (域名, (流量, URL))&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;18&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;&lt;span&gt;              &lt;/span&gt;&lt;/span&gt;&lt;span&gt;.&lt;/span&gt;&lt;span&gt;aggregateByKey&lt;/span&gt;&lt;span&gt;([], push, merge))&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;/code&gt;&lt;/pre&gt;&lt;div&gt;&lt;div&gt;&lt;/div&gt;&lt;div&gt;&lt;/div&gt;&lt;/div&gt;&lt;/figure&gt;&lt;div&gt;&lt;/div&gt;&lt;/div&gt;&lt;span&gt;展开&lt;/span&gt;&lt;span&gt;收起&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;p&gt;注意第一步不能省：同一个 URL 的日志分散在不同的分区里，必须先把它的总流量加起来，才能拿去比大小。第一步的 &lt;code&gt;reduceByKey&lt;/code&gt; 也是先在 Map 端局部求和再 shuffle，数据量同样会大幅收缩。&lt;/p&gt;&lt;/section&gt;
&lt;section&gt;&lt;h2&gt;五、多层级：上一层复用下一层的结果&lt;a href=&quot;#五多层级上一层复用下一层的结果&quot;&gt;&lt;span&gt;#&lt;/span&gt;&lt;/a&gt;&lt;/h2&gt;&lt;p&gt;URL 有一个很好的性质：&lt;strong&gt;每个 URL 只属于一个域名，每个域名只属于一个用户。&lt;/strong&gt;&lt;/p&gt;&lt;p&gt;结合上一节的道理：&lt;/p&gt;&lt;ul&gt;
&lt;li&gt;一个用户的 Top N URL，一定在他名下各个域名的 Top N URL 里；&lt;/li&gt;
&lt;li&gt;全局的 Top N URL，一定在所有域名的 Top N URL 里。&lt;/li&gt;
&lt;/ul&gt;&lt;p&gt;所以用户级和全局的 TopN 不需要从原始数据重新算，&lt;strong&gt;直接把域名级的结果按用户合并一次就行&lt;/strong&gt;：&lt;/p&gt;&lt;div&gt;&lt;figure&gt;&lt;figcaption&gt;&lt;/figcaption&gt;&lt;pre&gt;&lt;code&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;1&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;# domain_owner：域名 → 用户 的映射（广播出去）&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;2&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;&lt;span&gt;user_top &lt;/span&gt;&lt;span&gt;=&lt;/span&gt;&lt;span&gt; (domain_top&lt;/span&gt;&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;3&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;&lt;span&gt;            &lt;/span&gt;&lt;/span&gt;&lt;span&gt;.&lt;/span&gt;&lt;span&gt;map&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;lambda&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;kv&lt;/span&gt;&lt;span&gt;: (domain_owner.value[kv[&lt;/span&gt;&lt;span&gt;0&lt;/span&gt;&lt;span&gt;]], kv[&lt;/span&gt;&lt;span&gt;1&lt;/span&gt;&lt;span&gt;]))&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;4&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;&lt;span&gt;            &lt;/span&gt;&lt;/span&gt;&lt;span&gt;.&lt;/span&gt;&lt;span&gt;reduceByKey&lt;/span&gt;&lt;span&gt;(merge))&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;5&lt;/div&gt;&lt;/div&gt;&lt;div&gt;
&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;6&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;&lt;span&gt;global_top &lt;/span&gt;&lt;span&gt;=&lt;/span&gt;&lt;span&gt; domain_top.&lt;/span&gt;&lt;span&gt;values&lt;/span&gt;&lt;span&gt;().&lt;/span&gt;&lt;span&gt;reduce&lt;/span&gt;&lt;span&gt;(merge)&lt;/span&gt;&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;/code&gt;&lt;/pre&gt;&lt;div&gt;&lt;div&gt;&lt;/div&gt;&lt;div&gt;&lt;/div&gt;&lt;/div&gt;&lt;/figure&gt;&lt;/div&gt;&lt;p&gt;域名级的结果一共只有「域名数 × N」条，在这个基础上合并，开销几乎可以忽略。&lt;/p&gt;&lt;p&gt;&lt;strong&gt;但 Referer 和 User-Agent 不能这么做&lt;/strong&gt;：同一个 Referer 可能出现在很多个域名下，它在全局的总量，是在各个域名下的量加起来，而不在某个域名里排第几。一个 Referer 可能在每个域名下都排不进前 N，加起来却是全局第一。所以这类对象，在每个层级都要先按该层级的维度重新求和，再取 TopN。&lt;/p&gt;&lt;/section&gt;
&lt;section&gt;&lt;h2&gt;六、按流量和按请求数&lt;a href=&quot;#六按流量和按请求数&quot;&gt;&lt;span&gt;#&lt;/span&gt;&lt;/a&gt;&lt;/h2&gt;&lt;p&gt;URL 有两个排序指标：请求数和流量。这两个指标可以在一次遍历里同时计算：第一步求和时，同时累加请求数和流量，第二步分别维护两个堆。&lt;/p&gt;&lt;p&gt;为什么不单独做一个「按带宽排序」？因为在同一个统计时段里，带宽等于流量除以时间，两者的排序结果完全一样，没有必要重复计算。&lt;/p&gt;&lt;/section&gt;
&lt;section&gt;&lt;h2&gt;七、配置化：加指标不用重新发版&lt;a href=&quot;#七配置化加指标不用重新发版&quot;&gt;&lt;span&gt;#&lt;/span&gt;&lt;/a&gt;&lt;/h2&gt;&lt;p&gt;统计哪些对象、按什么指标、取多少个、在哪些层级统计，这些都写在一个 JSON 配置里，作为任务参数传入：&lt;/p&gt;&lt;div&gt;&lt;figure&gt;&lt;figcaption&gt;&lt;/figcaption&gt;&lt;pre&gt;&lt;code&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;1&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;{&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;2&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;  &lt;/span&gt;&lt;span&gt;&quot;top_n&quot;&lt;/span&gt;&lt;span&gt;: &lt;/span&gt;&lt;span&gt;100&lt;/span&gt;&lt;span&gt;,&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;3&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;  &lt;/span&gt;&lt;span&gt;&quot;levels&quot;&lt;/span&gt;&lt;span&gt;: [&lt;/span&gt;&lt;span&gt;&quot;domain&quot;&lt;/span&gt;&lt;span&gt;, &lt;/span&gt;&lt;span&gt;&quot;user&quot;&lt;/span&gt;&lt;span&gt;, &lt;/span&gt;&lt;span&gt;&quot;global&quot;&lt;/span&gt;&lt;span&gt;],&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;4&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;  &lt;/span&gt;&lt;span&gt;&quot;metrics&quot;&lt;/span&gt;&lt;span&gt;: [&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;5&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;&lt;span&gt;    &lt;/span&gt;&lt;/span&gt;&lt;span&gt;{ &lt;/span&gt;&lt;span&gt;&quot;name&quot;&lt;/span&gt;&lt;span&gt;: &lt;/span&gt;&lt;span&gt;&quot;top_url_pv&quot;&lt;/span&gt;&lt;span&gt;,      &lt;/span&gt;&lt;span&gt;&quot;key&quot;&lt;/span&gt;&lt;span&gt;: &lt;/span&gt;&lt;span&gt;&quot;url&quot;&lt;/span&gt;&lt;span&gt;,        &lt;/span&gt;&lt;span&gt;&quot;value&quot;&lt;/span&gt;&lt;span&gt;: &lt;/span&gt;&lt;span&gt;&quot;requests&quot;&lt;/span&gt;&lt;span&gt; },&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;6&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;&lt;span&gt;    &lt;/span&gt;&lt;/span&gt;&lt;span&gt;{ &lt;/span&gt;&lt;span&gt;&quot;name&quot;&lt;/span&gt;&lt;span&gt;: &lt;/span&gt;&lt;span&gt;&quot;top_url_traffic&quot;&lt;/span&gt;&lt;span&gt;, &lt;/span&gt;&lt;span&gt;&quot;key&quot;&lt;/span&gt;&lt;span&gt;: &lt;/span&gt;&lt;span&gt;&quot;url&quot;&lt;/span&gt;&lt;span&gt;,        &lt;/span&gt;&lt;span&gt;&quot;value&quot;&lt;/span&gt;&lt;span&gt;: &lt;/span&gt;&lt;span&gt;&quot;bytes&quot;&lt;/span&gt;&lt;span&gt; },&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;7&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;&lt;span&gt;    &lt;/span&gt;&lt;/span&gt;&lt;span&gt;{ &lt;/span&gt;&lt;span&gt;&quot;name&quot;&lt;/span&gt;&lt;span&gt;: &lt;/span&gt;&lt;span&gt;&quot;top_referer&quot;&lt;/span&gt;&lt;span&gt;,     &lt;/span&gt;&lt;span&gt;&quot;key&quot;&lt;/span&gt;&lt;span&gt;: &lt;/span&gt;&lt;span&gt;&quot;referer&quot;&lt;/span&gt;&lt;span&gt;,    &lt;/span&gt;&lt;span&gt;&quot;value&quot;&lt;/span&gt;&lt;span&gt;: &lt;/span&gt;&lt;span&gt;&quot;requests&quot;&lt;/span&gt;&lt;span&gt; },&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;8&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;&lt;span&gt;    &lt;/span&gt;&lt;/span&gt;&lt;span&gt;{ &lt;/span&gt;&lt;span&gt;&quot;name&quot;&lt;/span&gt;&lt;span&gt;: &lt;/span&gt;&lt;span&gt;&quot;top_ua&quot;&lt;/span&gt;&lt;span&gt;,          &lt;/span&gt;&lt;span&gt;&quot;key&quot;&lt;/span&gt;&lt;span&gt;: &lt;/span&gt;&lt;span&gt;&quot;user_agent&quot;&lt;/span&gt;&lt;span&gt;, &lt;/span&gt;&lt;span&gt;&quot;value&quot;&lt;/span&gt;&lt;span&gt;: &lt;/span&gt;&lt;span&gt;&quot;requests&quot;&lt;/span&gt;&lt;span&gt; }&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;9&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;&lt;span&gt;  &lt;/span&gt;&lt;/span&gt;&lt;span&gt;]&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;10&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;}&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;/code&gt;&lt;/pre&gt;&lt;div&gt;&lt;div&gt;&lt;/div&gt;&lt;div&gt;&lt;/div&gt;&lt;/div&gt;&lt;/figure&gt;&lt;/div&gt;&lt;p&gt;程序根据配置动态生成计算逻辑。运营想加一个新的统计维度，比如 Top 客户端 IP，只要改配置，不用重新打包、部署 Spark 程序。&lt;/p&gt;&lt;/section&gt;
&lt;section&gt;&lt;h2&gt;八、几个实践中的问题&lt;a href=&quot;#八几个实践中的问题&quot;&gt;&lt;span&gt;#&lt;/span&gt;&lt;/a&gt;&lt;/h2&gt;&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;数据倾斜&lt;/strong&gt;：头部大域名的日志量可能是普通域名的成千上万倍。好在 &lt;code&gt;reduceByKey&lt;/code&gt; 和 &lt;code&gt;aggregateByKey&lt;/code&gt; 都会先在 Map 端局部聚合，大域名的数据在进入 shuffle 之前就已经被压缩了，比 &lt;code&gt;groupByKey&lt;/code&gt; 稳定得多。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;URL 归一化&lt;/strong&gt;：同一个资源带上不同的查询参数（比如时间戳、签名），会被当成不同的 URL，把热点打散。统计前要按业务需要去掉无关参数，这一点和 CDN 缓存 key 的处理是一样的道理。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;N 要留余量&lt;/strong&gt;：如果控制台展示前 100，计算时可以多保留一些。这样后续按其他条件过滤（比如去掉某类请求）后，依然能凑够 100 条。&lt;/li&gt;
&lt;/ul&gt;&lt;/section&gt;
&lt;section&gt;&lt;h2&gt;九、总结&lt;a href=&quot;#九总结&quot;&gt;&lt;span&gt;#&lt;/span&gt;&lt;/a&gt;&lt;/h2&gt;&lt;p&gt;这个任务的核心就是一句话：&lt;strong&gt;只需要前 N 名的时候，就不要对全部数据排序。&lt;/strong&gt;&lt;/p&gt;&lt;ul&gt;
&lt;li&gt;单机上，用大小为 N 的小根堆，把内存固定在 N，把排序代价降到 O(M log N)；&lt;/li&gt;
&lt;li&gt;分布式下，利用「全局前 N 一定在各分区前 N 里」，先局部取 TopN 再合并，让 shuffle 的数据量收缩几个数量级；&lt;/li&gt;
&lt;li&gt;多层级时，对只属于一个上级的对象（URL），上层直接复用下层结果；对跨上级的对象（Referer、UA），每层单独求和。&lt;/li&gt;
&lt;/ul&gt;&lt;/section&gt;
&lt;section&gt;&lt;h2&gt;参考&lt;a href=&quot;#参考&quot;&gt;&lt;span&gt;#&lt;/span&gt;&lt;/a&gt;&lt;/h2&gt;&lt;ul&gt;
&lt;li&gt;&lt;a href=&quot;https://intl.cloud.baidu.com/doc/CDN/s/Hjwvyeb4z-en&quot; target=&quot;_blank&quot;&gt;百度智能云 CDN：TOP 类数据&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://spark.apache.org/docs/latest/api/python/reference/api/pyspark.RDD.aggregateByKey.html&quot; target=&quot;_blank&quot;&gt;PySpark aggregateByKey 文档&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;&lt;/section&gt;</content:encoded></item><item><title>RabbitMQ 发布订阅：CDN 配置下发和分布式账单任务的实践</title><link>https://tracerzzz.cn/posts/rabbitmq/</link><guid isPermaLink="true">https://tracerzzz.cn/posts/rabbitmq/</guid><description>我在中国电信用 RabbitMQ 做过 CDN 配置下发系统，在百度用 RabbitMQ + Celery 做过分布式账单任务。这篇整理 RabbitMQ 的核心概念、交换机类型怎么选，以及消息不丢、不重、不乱序的几种做法。</description><pubDate>Mon, 31 Dec 2018 16:00:00 GMT</pubDate><content:encoded>&lt;blockquote&gt;&lt;p&gt;&lt;strong&gt;关于这篇文章&lt;/strong&gt;：最初是 2019 年初在中国电信做 CDN 配置系统时留下的 RabbitMQ 安装笔记。2026 年 10 月重写：补充了后来在百度云 CDN 用 RabbitMQ + Celery 做分布式账单任务的经验，并整理了交换机选型和消息可靠性的做法。&lt;/p&gt;&lt;/blockquote&gt;
&lt;p&gt;我用 RabbitMQ 做过两个系统：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;中国电信 CDN 配置系统&lt;/strong&gt;：中心生成 Nginx 302 调度、ATS 缓存、DNS 等组件的配置，通过 RabbitMQ 下发到各个节点，节点上的 agent 收到后更新配置文件、重启对应的 Docker 服务；缓存刷新任务也走同一条链路。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;百度云 CDN 定制化账单&lt;/strong&gt;：每月给头部客户生成账单，任务通过 RabbitMQ 分发给多台机器并行计算，用 Celery 把「拉数据 → 计费 → 生成文件 → 上传存储 → 发邮件」编排成链式任务。&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;这篇从这两个场景出发，整理 RabbitMQ 的用法和几个关键问题。&lt;/p&gt;
&lt;section&gt;&lt;h2&gt;一、核心概念&lt;a href=&quot;#一核心概念&quot;&gt;&lt;span&gt;#&lt;/span&gt;&lt;/a&gt;&lt;/h2&gt;&lt;p&gt;RabbitMQ 里生产者不直接把消息发给队列，而是发给&lt;strong&gt;交换机（exchange）&lt;/strong&gt;，交换机按规则把消息路由到一个或多个&lt;strong&gt;队列（queue）&lt;/strong&gt;，消费者再从队列里取。&lt;/p&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;&lt;p&gt;routing key&lt;/p&gt;&lt;/span&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;&lt;p&gt;binding&lt;/p&gt;&lt;/span&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;&lt;p&gt;binding&lt;/p&gt;&lt;/span&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;&lt;/span&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;&lt;/span&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;&lt;p&gt;生产者&lt;/p&gt;&lt;/span&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;&lt;p&gt;交换机&lt;/p&gt;&lt;/span&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;&lt;p&gt;队列 1&lt;/p&gt;&lt;/span&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;&lt;p&gt;队列 2&lt;/p&gt;&lt;/span&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;&lt;p&gt;消费者 1&lt;/p&gt;&lt;/span&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;&lt;p&gt;消费者 2&lt;/p&gt;&lt;/span&gt;&lt;/div&gt;
&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;&lt;p&gt;routing key&lt;/p&gt;&lt;/span&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;&lt;p&gt;binding&lt;/p&gt;&lt;/span&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;&lt;p&gt;binding&lt;/p&gt;&lt;/span&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;&lt;/span&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;&lt;/span&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;&lt;p&gt;生产者&lt;/p&gt;&lt;/span&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;&lt;p&gt;交换机&lt;/p&gt;&lt;/span&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;&lt;p&gt;队列 1&lt;/p&gt;&lt;/span&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;&lt;p&gt;队列 2&lt;/p&gt;&lt;/span&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;&lt;p&gt;消费者 1&lt;/p&gt;&lt;/span&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;&lt;p&gt;消费者 2&lt;/p&gt;&lt;/span&gt;&lt;/div&gt;
&lt;/div&gt;&lt;/div&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;th&gt;适合的场景&lt;/th&gt;&lt;/tr&gt;&lt;/thead&gt;&lt;tbody&gt;&lt;tr&gt;&lt;td&gt;direct&lt;/td&gt;&lt;td&gt;routing key 完全匹配&lt;/td&gt;&lt;td&gt;点对点，把任务发给指定的一类消费者&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;fanout&lt;/td&gt;&lt;td&gt;忽略 routing key，发给所有绑定的队列&lt;/td&gt;&lt;td&gt;广播，所有节点都要收到&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;topic&lt;/td&gt;&lt;td&gt;routing key 按通配符匹配，&lt;code&gt;*&lt;/code&gt; 匹配一段，&lt;code&gt;#&lt;/code&gt; 匹配多段&lt;/td&gt;&lt;td&gt;按维度灵活订阅，比如按区域、按组件&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;headers&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;/section&gt;
&lt;section&gt;&lt;h2&gt;二、配置下发：用 topic 交换机做定向广播&lt;a href=&quot;#二配置下发用-topic-交换机做定向广播&quot;&gt;&lt;span&gt;#&lt;/span&gt;&lt;/a&gt;&lt;/h2&gt;&lt;p&gt;下面是按这个场景整理的思路和简化代码，不是当时的原始实现。&lt;/p&gt;&lt;p&gt;CDN 配置下发的特点是：一次变更，可能要发给全网所有节点，也可能只发给某个区域、某一类组件。用 topic 交换机，把 routing key 设计成「区域.组件」的形式，就能同时满足这两种需求：&lt;/p&gt;&lt;ul&gt;
&lt;li&gt;每个节点的 agent 建一个自己的持久化队列，按所在区域和运行的组件绑定，比如广东节点绑定 &lt;code&gt;guangdong.nginx&lt;/code&gt;、&lt;code&gt;guangdong.ats&lt;/code&gt;，再绑定全网广播用的 &lt;code&gt;all.nginx&lt;/code&gt;、&lt;code&gt;all.ats&lt;/code&gt;；&lt;/li&gt;
&lt;li&gt;只改广东节点的 ATS 配置，发 &lt;code&gt;guangdong.ats&lt;/code&gt;；&lt;/li&gt;
&lt;li&gt;全网的 Nginx 都要改，发 &lt;code&gt;all.nginx&lt;/code&gt;；&lt;/li&gt;
&lt;li&gt;以后要加一个维度（比如运营商），也只是在 routing key 里多加一段，通配符 &lt;code&gt;*&lt;/code&gt;、&lt;code&gt;#&lt;/code&gt; 可以按需匹配。&lt;/li&gt;
&lt;/ul&gt;&lt;p&gt;agent 端的处理流程（Python + pika，简化版）：&lt;/p&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;&lt;figure&gt;&lt;figcaption&gt;&lt;/figcaption&gt;&lt;pre&gt;&lt;code&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;1&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;import&lt;/span&gt;&lt;span&gt; json&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;2&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;import&lt;/span&gt;&lt;span&gt; pika&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;3&lt;/div&gt;&lt;/div&gt;&lt;div&gt;
&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;4&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;&lt;span&gt;conn &lt;/span&gt;&lt;span&gt;=&lt;/span&gt;&lt;span&gt; pika.&lt;/span&gt;&lt;span&gt;BlockingConnection&lt;/span&gt;&lt;span&gt;(pika.&lt;/span&gt;&lt;span&gt;ConnectionParameters&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;/span&gt;&lt;span&gt;&quot;mq-host&quot;&lt;/span&gt;&lt;span&gt;))&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;5&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;&lt;span&gt;ch &lt;/span&gt;&lt;span&gt;=&lt;/span&gt;&lt;span&gt; conn.&lt;/span&gt;&lt;span&gt;channel&lt;/span&gt;&lt;span&gt;()&lt;/span&gt;&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;6&lt;/div&gt;&lt;/div&gt;&lt;div&gt;
&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;7&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;&lt;span&gt;ch.&lt;/span&gt;&lt;span&gt;exchange_declare&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;/span&gt;&lt;span&gt;exchange&lt;/span&gt;&lt;span&gt;=&lt;/span&gt;&lt;span&gt;&quot;cdn.config&quot;&lt;/span&gt;&lt;span&gt;, &lt;/span&gt;&lt;span&gt;exchange_type&lt;/span&gt;&lt;span&gt;=&lt;/span&gt;&lt;span&gt;&quot;topic&quot;&lt;/span&gt;&lt;span&gt;, &lt;/span&gt;&lt;span&gt;durable&lt;/span&gt;&lt;span&gt;=&lt;/span&gt;&lt;span&gt;True&lt;/span&gt;&lt;span&gt;)&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;8&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;&lt;span&gt;q &lt;/span&gt;&lt;span&gt;=&lt;/span&gt;&lt;span&gt; ch.&lt;/span&gt;&lt;span&gt;queue_declare&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;/span&gt;&lt;span&gt;queue&lt;/span&gt;&lt;span&gt;=&lt;/span&gt;&lt;span&gt;&quot;agent.node-001&quot;&lt;/span&gt;&lt;span&gt;, &lt;/span&gt;&lt;span&gt;durable&lt;/span&gt;&lt;span&gt;=&lt;/span&gt;&lt;span&gt;True&lt;/span&gt;&lt;span&gt;).method.queue&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;9&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;for&lt;/span&gt;&lt;span&gt; key &lt;/span&gt;&lt;span&gt;in&lt;/span&gt;&lt;span&gt; [&lt;/span&gt;&lt;span&gt;&quot;guangdong.nginx&quot;&lt;/span&gt;&lt;span&gt;, &lt;/span&gt;&lt;span&gt;&quot;guangdong.ats&quot;&lt;/span&gt;&lt;span&gt;, &lt;/span&gt;&lt;span&gt;&quot;all.nginx&quot;&lt;/span&gt;&lt;span&gt;, &lt;/span&gt;&lt;span&gt;&quot;all.ats&quot;&lt;/span&gt;&lt;span&gt;]:&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;10&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;&lt;span&gt;    &lt;/span&gt;&lt;/span&gt;&lt;span&gt;ch.&lt;/span&gt;&lt;span&gt;queue_bind&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;exchange&lt;/span&gt;&lt;span&gt;=&lt;/span&gt;&lt;span&gt;&quot;cdn.config&quot;&lt;/span&gt;&lt;span&gt;, &lt;/span&gt;&lt;span&gt;queue&lt;/span&gt;&lt;span&gt;&lt;span&gt;=&lt;/span&gt;&lt;span&gt;q, &lt;/span&gt;&lt;/span&gt;&lt;span&gt;routing_key&lt;/span&gt;&lt;span&gt;&lt;span&gt;=&lt;/span&gt;&lt;span&gt;key)&lt;/span&gt;&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;11&lt;/div&gt;&lt;/div&gt;&lt;div&gt;
&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;12&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;&lt;span&gt;ch.&lt;/span&gt;&lt;span&gt;basic_qos&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;/span&gt;&lt;span&gt;prefetch_count&lt;/span&gt;&lt;span&gt;=&lt;/span&gt;&lt;span&gt;1&lt;/span&gt;&lt;span&gt;)  &lt;/span&gt;&lt;span&gt;# 一次只取一条，处理完再取下一条&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;13&lt;/div&gt;&lt;/div&gt;&lt;div&gt;
&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;14&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;def&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;on_message&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;ch&lt;/span&gt;&lt;span&gt;,&lt;/span&gt;&lt;span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;method&lt;/span&gt;&lt;/span&gt;&lt;span&gt;,&lt;/span&gt;&lt;span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;props&lt;/span&gt;&lt;/span&gt;&lt;span&gt;,&lt;/span&gt;&lt;span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;body&lt;/span&gt;&lt;/span&gt;&lt;span&gt;):&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;15&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;&lt;span&gt;    &lt;/span&gt;&lt;/span&gt;&lt;span&gt;task &lt;/span&gt;&lt;span&gt;=&lt;/span&gt;&lt;span&gt; json.&lt;/span&gt;&lt;span&gt;loads&lt;/span&gt;&lt;span&gt;(body)&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;16&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;    &lt;/span&gt;&lt;span&gt;try&lt;/span&gt;&lt;span&gt;:&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;17&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;        &lt;/span&gt;&lt;span&gt;if&lt;/span&gt;&lt;span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;already_applied&lt;/span&gt;&lt;span&gt;(task[&lt;/span&gt;&lt;/span&gt;&lt;span&gt;&quot;version&quot;&lt;/span&gt;&lt;span&gt;]):     &lt;/span&gt;&lt;span&gt;# 幂等：同一版本配置只应用一次&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;18&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;&lt;span&gt;            &lt;/span&gt;&lt;/span&gt;&lt;span&gt;ch.&lt;/span&gt;&lt;span&gt;basic_ack&lt;/span&gt;&lt;span&gt;(method.delivery_tag)&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;19&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;            &lt;/span&gt;&lt;span&gt;return&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;20&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;&lt;span&gt;        &lt;/span&gt;&lt;/span&gt;&lt;span&gt;write_config&lt;/span&gt;&lt;span&gt;(task[&lt;/span&gt;&lt;span&gt;&quot;component&quot;&lt;/span&gt;&lt;span&gt;], task[&lt;/span&gt;&lt;span&gt;&quot;content&quot;&lt;/span&gt;&lt;span&gt;])&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;21&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;&lt;span&gt;        &lt;/span&gt;&lt;/span&gt;&lt;span&gt;reload_service&lt;/span&gt;&lt;span&gt;(task[&lt;/span&gt;&lt;span&gt;&quot;component&quot;&lt;/span&gt;&lt;span&gt;])         &lt;/span&gt;&lt;span&gt;# 例如 docker restart / nginx -s reload&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;22&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;&lt;span&gt;        &lt;/span&gt;&lt;/span&gt;&lt;span&gt;report&lt;/span&gt;&lt;span&gt;(task[&lt;/span&gt;&lt;span&gt;&quot;id&quot;&lt;/span&gt;&lt;span&gt;], &lt;/span&gt;&lt;span&gt;&quot;success&quot;&lt;/span&gt;&lt;span&gt;)&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;23&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;&lt;span&gt;        &lt;/span&gt;&lt;/span&gt;&lt;span&gt;ch.&lt;/span&gt;&lt;span&gt;basic_ack&lt;/span&gt;&lt;span&gt;(method.delivery_tag)         &lt;/span&gt;&lt;span&gt;# 处理成功才确认&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;24&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;    &lt;/span&gt;&lt;span&gt;except&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;Exception&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;as&lt;/span&gt;&lt;span&gt; e:&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;25&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;&lt;span&gt;        &lt;/span&gt;&lt;/span&gt;&lt;span&gt;report&lt;/span&gt;&lt;span&gt;(task[&lt;/span&gt;&lt;span&gt;&quot;id&quot;&lt;/span&gt;&lt;span&gt;], &lt;/span&gt;&lt;span&gt;&quot;failed&quot;&lt;/span&gt;&lt;span&gt;, &lt;/span&gt;&lt;span&gt;str&lt;/span&gt;&lt;span&gt;(e))&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;26&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;&lt;span&gt;        &lt;/span&gt;&lt;/span&gt;&lt;span&gt;ch.&lt;/span&gt;&lt;span&gt;basic_nack&lt;/span&gt;&lt;span&gt;(method.delivery_tag, &lt;/span&gt;&lt;span&gt;requeue&lt;/span&gt;&lt;span&gt;=&lt;/span&gt;&lt;span&gt;False&lt;/span&gt;&lt;span&gt;)  &lt;/span&gt;&lt;span&gt;# 交给死信队列，避免无限重试&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;27&lt;/div&gt;&lt;/div&gt;&lt;div&gt;
&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;28&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;&lt;span&gt;ch.&lt;/span&gt;&lt;span&gt;basic_consume&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;/span&gt;&lt;span&gt;queue&lt;/span&gt;&lt;span&gt;&lt;span&gt;=&lt;/span&gt;&lt;span&gt;q, &lt;/span&gt;&lt;/span&gt;&lt;span&gt;on_message_callback&lt;/span&gt;&lt;span&gt;&lt;span&gt;=&lt;/span&gt;&lt;span&gt;on_message)&lt;/span&gt;&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;29&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;&lt;span&gt;ch.&lt;/span&gt;&lt;span&gt;start_consuming&lt;/span&gt;&lt;span&gt;()&lt;/span&gt;&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;/code&gt;&lt;/pre&gt;&lt;div&gt;&lt;div&gt;&lt;/div&gt;&lt;div&gt;&lt;/div&gt;&lt;/div&gt;&lt;/figure&gt;&lt;div&gt;&lt;/div&gt;&lt;/div&gt;&lt;span&gt;展开&lt;/span&gt;&lt;span&gt;收起&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;p&gt;这段代码里体现了配置下发最关心的三件事：&lt;/p&gt;&lt;ol&gt;
&lt;li&gt;&lt;strong&gt;手动确认（ack）&lt;/strong&gt;：改完配置、重启成功之后才 ack。agent 处理到一半挂掉，消息会重新投递，不会丢。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;幂等&lt;/strong&gt;：重新投递意味着同一条消息可能执行两次。每份配置带一个版本号，已经应用过的版本直接跳过。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;结果回报&lt;/strong&gt;：中心要知道每个节点到底改没改成功，所以 agent 处理完要上报结果，失败的单独重试或者人工介入。&lt;/li&gt;
&lt;/ol&gt;&lt;/section&gt;
&lt;section&gt;&lt;h2&gt;三、分布式账单：RabbitMQ + Celery 的链式任务&lt;a href=&quot;#三分布式账单rabbitmq--celery-的链式任务&quot;&gt;&lt;span&gt;#&lt;/span&gt;&lt;/a&gt;&lt;/h2&gt;&lt;p&gt;账单场景和配置下发不同：不是「所有节点都要收到」，而是「一堆任务，谁空闲谁来做」。这正好是工作队列模式：多个 worker 消费同一个队列，RabbitMQ 按轮询把任务分给它们，&lt;code&gt;prefetch_count&lt;/code&gt; 控制每个 worker 手上最多压几个任务，避免慢的 worker 囤积任务。&lt;/p&gt;&lt;p&gt;直接用 pika 写任务分发、重试、结果收集比较繁琐，所以用了 &lt;a href=&quot;https://docs.celeryq.dev/&quot; target=&quot;_blank&quot;&gt;Celery&lt;/a&gt;，RabbitMQ 作为它的 broker。一个客户的出账流程是几个有先后依赖的步骤，用 Celery 的 &lt;code&gt;chain&lt;/code&gt; 串起来，上一步的返回值自动作为下一步的输入：&lt;/p&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;&lt;figure&gt;&lt;figcaption&gt;&lt;/figcaption&gt;&lt;pre&gt;&lt;code&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;1&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;from&lt;/span&gt;&lt;span&gt; celery &lt;/span&gt;&lt;span&gt;import&lt;/span&gt;&lt;span&gt; Celery, chain&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;2&lt;/div&gt;&lt;/div&gt;&lt;div&gt;
&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;3&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;&lt;span&gt;app &lt;/span&gt;&lt;span&gt;=&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;Celery&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;/span&gt;&lt;span&gt;&quot;billing&quot;&lt;/span&gt;&lt;span&gt;, &lt;/span&gt;&lt;span&gt;broker&lt;/span&gt;&lt;span&gt;=&lt;/span&gt;&lt;span&gt;&quot;amqp://user:pass@mq-host//&quot;&lt;/span&gt;&lt;span&gt;)&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;4&lt;/div&gt;&lt;/div&gt;&lt;div&gt;
&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;5&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;&lt;span&gt;@app&lt;/span&gt;&lt;span&gt;.&lt;/span&gt;&lt;span&gt;task&lt;/span&gt;&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;bind&lt;/span&gt;&lt;span&gt;=&lt;/span&gt;&lt;span&gt;True&lt;/span&gt;&lt;span&gt;&lt;span&gt;,&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;/span&gt;&lt;span&gt;max_retries&lt;/span&gt;&lt;span&gt;=&lt;/span&gt;&lt;span&gt;3&lt;/span&gt;&lt;span&gt;&lt;span&gt;,&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;/span&gt;&lt;span&gt;default_retry_delay&lt;/span&gt;&lt;span&gt;=&lt;/span&gt;&lt;span&gt;60&lt;/span&gt;&lt;span&gt;)&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;6&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;def&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;fetch_usage&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;self&lt;/span&gt;&lt;span&gt;,&lt;/span&gt;&lt;span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;customer&lt;/span&gt;&lt;/span&gt;&lt;span&gt;,&lt;/span&gt;&lt;span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;month&lt;/span&gt;&lt;/span&gt;&lt;span&gt;):&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;7&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;    &lt;/span&gt;&lt;span&gt;try&lt;/span&gt;&lt;span&gt;:&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;8&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;        &lt;/span&gt;&lt;span&gt;return&lt;/span&gt;&lt;span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;query_usage&lt;/span&gt;&lt;span&gt;(customer, month)      &lt;/span&gt;&lt;/span&gt;&lt;span&gt;# 拉取域名级带宽、流量打点&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;9&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;    &lt;/span&gt;&lt;span&gt;except&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;TimeoutError&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;as&lt;/span&gt;&lt;span&gt; e:&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;10&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;        &lt;/span&gt;&lt;span&gt;raise&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;self&lt;/span&gt;&lt;span&gt;&lt;span&gt;.&lt;/span&gt;&lt;span&gt;retry&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;/span&gt;&lt;span&gt;exc&lt;/span&gt;&lt;span&gt;&lt;span&gt;=&lt;/span&gt;&lt;span&gt;e)                  &lt;/span&gt;&lt;/span&gt;&lt;span&gt;# 数据源超时，延迟 60 秒重试，最多 3 次&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;11&lt;/div&gt;&lt;/div&gt;&lt;div&gt;
&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;12&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;&lt;span&gt;@app&lt;/span&gt;&lt;span&gt;.&lt;/span&gt;&lt;span&gt;task&lt;/span&gt;&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;13&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;def&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;calc_bill&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;usage&lt;/span&gt;&lt;span&gt;):&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;14&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;    &lt;/span&gt;&lt;span&gt;return&lt;/span&gt;&lt;span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;apply_billing_rules&lt;/span&gt;&lt;span&gt;(usage)            &lt;/span&gt;&lt;/span&gt;&lt;span&gt;# 95 峰值、日峰月均、闲忙时等计费方式&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;15&lt;/div&gt;&lt;/div&gt;&lt;div&gt;
&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;16&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;&lt;span&gt;@app&lt;/span&gt;&lt;span&gt;.&lt;/span&gt;&lt;span&gt;task&lt;/span&gt;&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;17&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;def&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;render_and_upload&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;bill&lt;/span&gt;&lt;span&gt;):&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;18&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;    &lt;/span&gt;&lt;span&gt;return&lt;/span&gt;&lt;span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;upload&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;render_file&lt;/span&gt;&lt;span&gt;(bill))             &lt;/span&gt;&lt;/span&gt;&lt;span&gt;# 按客户要求的格式生成文件并上传&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;19&lt;/div&gt;&lt;/div&gt;&lt;div&gt;
&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;20&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;&lt;span&gt;@app&lt;/span&gt;&lt;span&gt;.&lt;/span&gt;&lt;span&gt;task&lt;/span&gt;&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;21&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;def&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;send_mail&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;file_url&lt;/span&gt;&lt;span&gt;):&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;22&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;&lt;span&gt;    &lt;/span&gt;&lt;/span&gt;&lt;span&gt;notify&lt;/span&gt;&lt;span&gt;(file_url)&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;23&lt;/div&gt;&lt;/div&gt;&lt;div&gt;
&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;24&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;# 每个客户一条链，所有客户的链并行跑在多台 worker 上&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;25&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;for&lt;/span&gt;&lt;span&gt; customer &lt;/span&gt;&lt;span&gt;in&lt;/span&gt;&lt;span&gt; customers:&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;26&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;&lt;span&gt;    &lt;/span&gt;&lt;/span&gt;&lt;span&gt;chain&lt;/span&gt;&lt;span&gt;(fetch_usage.&lt;/span&gt;&lt;span&gt;s&lt;/span&gt;&lt;span&gt;(customer, &lt;/span&gt;&lt;span&gt;&quot;2020-10&quot;&lt;/span&gt;&lt;span&gt;&lt;span&gt;), calc_bill.&lt;/span&gt;&lt;span&gt;s&lt;/span&gt;&lt;span&gt;(),&lt;/span&gt;&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;27&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;&lt;span&gt;          &lt;/span&gt;&lt;/span&gt;&lt;span&gt;render_and_upload.&lt;/span&gt;&lt;span&gt;s&lt;/span&gt;&lt;span&gt;(), send_mail.&lt;/span&gt;&lt;span&gt;s&lt;/span&gt;&lt;span&gt;()).&lt;/span&gt;&lt;span&gt;apply_async&lt;/span&gt;&lt;span&gt;()&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;/code&gt;&lt;/pre&gt;&lt;div&gt;&lt;div&gt;&lt;/div&gt;&lt;div&gt;&lt;/div&gt;&lt;/div&gt;&lt;/figure&gt;&lt;div&gt;&lt;/div&gt;&lt;/div&gt;&lt;span&gt;展开&lt;/span&gt;&lt;span&gt;收起&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;p&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;每一步都要幂等&lt;/strong&gt;：重试意味着同一步可能执行多次，上传文件用固定的文件名覆盖，发邮件前检查是否已发送。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;定时校验&lt;/strong&gt;：链式任务跑完不代表结果对。当时还有一个定时任务，检查账单数据和源数据是否一致，不一致就重新触发。&lt;/li&gt;
&lt;/ul&gt;&lt;/section&gt;
&lt;section&gt;&lt;h2&gt;四、消息不丢、不重、不乱序&lt;a href=&quot;#四消息不丢不重不乱序&quot;&gt;&lt;span&gt;#&lt;/span&gt;&lt;/a&gt;&lt;/h2&gt;&lt;p&gt;这是用消息队列绕不开的三个问题，整理一下各自的手段：&lt;/p&gt;&lt;p&gt;&lt;strong&gt;不丢&lt;/strong&gt;要三端一起保证：&lt;/p&gt;&lt;ul&gt;
&lt;li&gt;生产者：开启 &lt;strong&gt;publisher confirm&lt;/strong&gt;，broker 确认收到之后才算发送成功；&lt;/li&gt;
&lt;li&gt;broker：交换机、队列声明为 &lt;code&gt;durable&lt;/code&gt;，消息设为持久化（&lt;code&gt;delivery_mode=2&lt;/code&gt;），重启后不丢；&lt;/li&gt;
&lt;li&gt;消费者：关闭自动确认，&lt;strong&gt;处理成功后手动 ack&lt;/strong&gt;。&lt;/li&gt;
&lt;/ul&gt;&lt;p&gt;&lt;strong&gt;不重&lt;/strong&gt;：RabbitMQ 本身只能做到「至少一次」投递，重复是不可避免的，只能靠消费端&lt;strong&gt;幂等&lt;/strong&gt;解决，比如用业务 ID 或版本号去重。&lt;/p&gt;&lt;p&gt;&lt;strong&gt;不乱序&lt;/strong&gt;：单个队列、单个消费者时消息是有序的；一旦多个消费者并发处理，或者消息被重新投递，顺序就无法保证。配置下发这种场景，与其强求顺序，不如每份配置带上版本号，&lt;strong&gt;只应用比当前更新的版本&lt;/strong&gt;，乱序到达的旧版本直接丢弃。&lt;/p&gt;&lt;/section&gt;
&lt;section&gt;&lt;h2&gt;五、本地快速启动&lt;a href=&quot;#五本地快速启动&quot;&gt;&lt;span&gt;#&lt;/span&gt;&lt;/a&gt;&lt;/h2&gt;&lt;div&gt;&lt;figure&gt;&lt;figcaption&gt;&lt;span&gt;&lt;/span&gt;&lt;span&gt;Terminal window&lt;/span&gt;&lt;/figcaption&gt;&lt;pre&gt;&lt;code&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;1&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;# 带管理界面的镜像，15672 是 Web 管理端口&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;2&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;docker&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;run&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;-d&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;--name&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;rabbitmq&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;-p&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;5672:5672&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;-p&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;15672:15672&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;rabbitmq:3-management&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;3&lt;/div&gt;&lt;/div&gt;&lt;div&gt;
&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;4&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;# 默认的 guest 账号只能从本机登录，远程连接要新建用户&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;5&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;docker&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;exec&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;rabbitmq&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;rabbitmqctl&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;add_user&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;&amp;lt;user&amp;gt;&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;&amp;lt;password&amp;gt;&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;6&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;docker&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;exec&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;rabbitmq&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;rabbitmqctl&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;set_user_tags&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;&amp;lt;user&amp;gt;&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;administrator&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;7&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;docker&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;exec&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;rabbitmq&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;rabbitmqctl&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;set_permissions&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;-p&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;/&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;&amp;lt;user&amp;gt;&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;&quot;.*&quot;&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;&quot;.*&quot;&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;&quot;.*&quot;&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;/code&gt;&lt;/pre&gt;&lt;div&gt;&lt;div&gt;&lt;/div&gt;&lt;div&gt;&lt;/div&gt;&lt;/div&gt;&lt;/figure&gt;&lt;/div&gt;&lt;p&gt;注意 &lt;code&gt;set_permissions&lt;/code&gt; 的三个参数是正则表达式，分别表示配置、写、读权限，&lt;code&gt;&quot;.*&quot;&lt;/code&gt; 才是「全部」；只写 &lt;code&gt;&quot;.&quot;&lt;/code&gt; 匹配的是长度为 1 的资源名。&lt;/p&gt;&lt;/section&gt;
&lt;section&gt;&lt;h2&gt;参考&lt;a href=&quot;#参考&quot;&gt;&lt;span&gt;#&lt;/span&gt;&lt;/a&gt;&lt;/h2&gt;&lt;ul&gt;
&lt;li&gt;&lt;a href=&quot;https://www.rabbitmq.com/tutorials&quot; target=&quot;_blank&quot;&gt;RabbitMQ Tutorials&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://www.rabbitmq.com/docs/reliability&quot; target=&quot;_blank&quot;&gt;RabbitMQ 可靠性指南&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://docs.celeryq.dev/en/stable/userguide/canvas.html&quot; target=&quot;_blank&quot;&gt;Celery Canvas：chain、group、chord&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;&lt;/section&gt;</content:encoded></item><item><title>使用scrapy+scrapy-splash爬取网易云音乐歌单</title><link>https://tracerzzz.cn/posts/scrapy-netease-music-playlist/</link><guid isPermaLink="true">https://tracerzzz.cn/posts/scrapy-netease-music-playlist/</guid><description>在使用网易云音乐的时候，你是否想过大家都在听些什么，什么音乐被听得最多，作为一枚程序员，肯定是自己动手丰衣足食啦，那么，这篇文正将会告诉你如何使用scrapy+scrapy-splash来爬取网易云音乐的歌单，然后存入mongodb，通过播放量找到大家听得最多的歌单。</description><pubDate>Wed, 02 May 2018 08:37:11 GMT</pubDate><content:encoded>&lt;blockquote&gt;&lt;p&gt;这是 2018 年写的旧文。网易云音乐的页面和接口此后变化很大，文中的选择器和地址大概率已经失效，这里只作为 Scrapy + Splash 抓取动态页面的思路参考。&lt;/p&gt;&lt;/blockquote&gt;
&lt;p&gt;&lt;/p&gt;&lt;figure&gt;&lt;img alt=&quot;2018051515263513332302.png&quot; loading=&quot;lazy&quot; width=&quot;806&quot; height=&quot;424&quot; src=&quot;/_astro/2018051515263513332302.BvmZS7HI_ZnpdXS.webp&quot; /&gt;&lt;figcaption&gt;2018051515263513332302.png&lt;/figcaption&gt;&lt;/figure&gt;&lt;p&gt;&lt;/p&gt;
&lt;section&gt;&lt;h3&gt;创建项目&lt;a href=&quot;#创建项目&quot;&gt;&lt;span&gt;#&lt;/span&gt;&lt;/a&gt;&lt;/h3&gt;&lt;div&gt;&lt;figure&gt;&lt;figcaption&gt;&lt;span&gt;&lt;/span&gt;&lt;span&gt;Terminal window&lt;/span&gt;&lt;/figcaption&gt;&lt;pre&gt;&lt;code&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;1&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;scrapy&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;startproject&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;music163Scrapy&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;2&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;cd&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;music163Scrapy&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;3&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;scrapy&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;genspider&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;Music163&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;163.com&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;/code&gt;&lt;/pre&gt;&lt;div&gt;&lt;div&gt;&lt;/div&gt;&lt;div&gt;&lt;/div&gt;&lt;/div&gt;&lt;/figure&gt;&lt;/div&gt;&lt;p&gt;文件目录如下：&lt;/p&gt;&lt;p&gt;&lt;/p&gt;&lt;figure&gt;&lt;img alt=&quot;20180515152636349896977.png&quot; loading=&quot;lazy&quot; width=&quot;1534&quot; height=&quot;862&quot; src=&quot;/_astro/20180515152636349896977.BEOsK8gg_Z1G4YAj.webp&quot; /&gt;&lt;figcaption&gt;20180515152636349896977.png&lt;/figcaption&gt;&lt;/figure&gt;&lt;p&gt;&lt;/p&gt;&lt;/section&gt;
&lt;section&gt;&lt;h3&gt;Items.py&lt;a href=&quot;#itemspy&quot;&gt;&lt;span&gt;#&lt;/span&gt;&lt;/a&gt;&lt;/h3&gt;&lt;p&gt;生成项目目录之后就可以开始编写代码啦，首先修改items.py，定义我们爬取网页后收集的字段。&lt;/p&gt;&lt;div&gt;&lt;figure&gt;&lt;figcaption&gt;&lt;/figcaption&gt;&lt;pre&gt;&lt;code&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;1&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;import&lt;/span&gt;&lt;span&gt; scrapy&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;2&lt;/div&gt;&lt;/div&gt;&lt;div&gt;
&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;3&lt;/div&gt;&lt;/div&gt;&lt;div&gt;
&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;4&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;class&lt;/span&gt;&lt;span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;Scrapymusic163Item&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;scrapy&lt;/span&gt;&lt;span&gt;.&lt;/span&gt;&lt;span&gt;Item&lt;/span&gt;&lt;span&gt;)&lt;/span&gt;&lt;/span&gt;&lt;span&gt;:&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;5&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;    &lt;/span&gt;&lt;span&gt;# define the fields for your item here like:&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;6&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;    &lt;/span&gt;&lt;span&gt;# name = scrapy.Field()&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;7&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;&lt;span&gt;    &lt;/span&gt;&lt;/span&gt;&lt;span&gt;title &lt;/span&gt;&lt;span&gt;=&lt;/span&gt;&lt;span&gt; scrapy.&lt;/span&gt;&lt;span&gt;Field&lt;/span&gt;&lt;span&gt;()  &lt;/span&gt;&lt;span&gt;# 标题&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;8&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;&lt;span&gt;    &lt;/span&gt;&lt;/span&gt;&lt;span&gt;link &lt;/span&gt;&lt;span&gt;=&lt;/span&gt;&lt;span&gt; scrapy.&lt;/span&gt;&lt;span&gt;Field&lt;/span&gt;&lt;span&gt;()  &lt;/span&gt;&lt;span&gt;# 链接&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;9&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;&lt;span&gt;    &lt;/span&gt;&lt;/span&gt;&lt;span&gt;realLink&lt;/span&gt;&lt;span&gt;=&lt;/span&gt;&lt;span&gt;scrapy.&lt;/span&gt;&lt;span&gt;Field&lt;/span&gt;&lt;span&gt;() &lt;/span&gt;&lt;span&gt;#完整连接&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;10&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;&lt;span&gt;    &lt;/span&gt;&lt;/span&gt;&lt;span&gt;desc &lt;/span&gt;&lt;span&gt;=&lt;/span&gt;&lt;span&gt; scrapy.&lt;/span&gt;&lt;span&gt;Field&lt;/span&gt;&lt;span&gt;()  &lt;/span&gt;&lt;span&gt;# 简述&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;11&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;&lt;span&gt;    &lt;/span&gt;&lt;/span&gt;&lt;span&gt;listenCount &lt;/span&gt;&lt;span&gt;=&lt;/span&gt;&lt;span&gt; scrapy.&lt;/span&gt;&lt;span&gt;Field&lt;/span&gt;&lt;span&gt;() &lt;/span&gt;&lt;span&gt;#播放数量&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;12&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;&lt;span&gt;    &lt;/span&gt;&lt;/span&gt;&lt;span&gt;posttime &lt;/span&gt;&lt;span&gt;=&lt;/span&gt;&lt;span&gt; scrapy.&lt;/span&gt;&lt;span&gt;Field&lt;/span&gt;&lt;span&gt;()  &lt;/span&gt;&lt;span&gt;# 发布时间&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;13&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;&lt;span&gt;    &lt;/span&gt;&lt;/span&gt;&lt;span&gt;imgUrl&lt;/span&gt;&lt;span&gt;=&lt;/span&gt;&lt;span&gt;scrapy.&lt;/span&gt;&lt;span&gt;Field&lt;/span&gt;&lt;span&gt;()  &lt;/span&gt;&lt;span&gt;# 歌单封面图片url&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;14&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;    &lt;/span&gt;&lt;span&gt;# pass&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;/code&gt;&lt;/pre&gt;&lt;div&gt;&lt;div&gt;&lt;/div&gt;&lt;div&gt;&lt;/div&gt;&lt;/div&gt;&lt;/figure&gt;&lt;/div&gt;&lt;/section&gt;
&lt;section&gt;&lt;h3&gt;Music163Spider&lt;a href=&quot;#music163spider&quot;&gt;&lt;span&gt;#&lt;/span&gt;&lt;/a&gt;&lt;/h3&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;&lt;figure&gt;&lt;figcaption&gt;&lt;/figcaption&gt;&lt;pre&gt;&lt;code&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;1&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;class&lt;/span&gt;&lt;span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;Music163Spider&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;scrapy&lt;/span&gt;&lt;span&gt;.&lt;/span&gt;&lt;span&gt;Spider&lt;/span&gt;&lt;span&gt;)&lt;/span&gt;&lt;/span&gt;&lt;span&gt;:&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;2&lt;/div&gt;&lt;/div&gt;&lt;div&gt;
&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;3&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;&lt;span&gt;    &lt;/span&gt;&lt;/span&gt;&lt;span&gt;name &lt;/span&gt;&lt;span&gt;=&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;&quot;music163&quot;&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;4&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;&lt;span&gt;    &lt;/span&gt;&lt;/span&gt;&lt;span&gt;allowed_domains &lt;/span&gt;&lt;span&gt;=&lt;/span&gt;&lt;span&gt; [&lt;/span&gt;&lt;span&gt;&quot;163.com&quot;&lt;/span&gt;&lt;span&gt;]&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;5&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;&lt;span&gt;    &lt;/span&gt;&lt;/span&gt;&lt;span&gt;start_urls &lt;/span&gt;&lt;span&gt;=&lt;/span&gt;&lt;span&gt; [&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;6&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;        &lt;/span&gt;&lt;span&gt;&quot;https://music.163.com/discover/playlist/?order=new&quot;&lt;/span&gt;&lt;span&gt;,&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;7&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;        &lt;/span&gt;&lt;span&gt;&quot;https://music.163.com/#/discover/playlist/&quot;&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;8&lt;/div&gt;&lt;/div&gt;&lt;div&gt;
&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;9&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;&lt;span&gt;    &lt;/span&gt;&lt;/span&gt;&lt;span&gt;]&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;10&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;    &lt;/span&gt;&lt;span&gt;def&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;parse&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;self&lt;/span&gt;&lt;span&gt;,&lt;/span&gt;&lt;span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;response&lt;/span&gt;&lt;/span&gt;&lt;span&gt;):&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;11&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;        &lt;/span&gt;&lt;span&gt;for&lt;/span&gt;&lt;span&gt; url &lt;/span&gt;&lt;span&gt;in&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;self&lt;/span&gt;&lt;span&gt;.start_urls:&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;12&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;            &lt;/span&gt;&lt;span&gt;yield&lt;/span&gt;&lt;span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;SplashRequest&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;/span&gt;&lt;span&gt;url&lt;/span&gt;&lt;span&gt;&lt;span&gt;=&lt;/span&gt;&lt;span&gt;url, &lt;/span&gt;&lt;/span&gt;&lt;span&gt;callback&lt;/span&gt;&lt;span&gt;=&lt;/span&gt;&lt;span&gt;self&lt;/span&gt;&lt;span&gt;.parseRequest,&lt;/span&gt;&lt;span&gt;endpoint&lt;/span&gt;&lt;span&gt;=&lt;/span&gt;&lt;span&gt;&apos;render.json&apos;&lt;/span&gt;&lt;span&gt;, &lt;/span&gt;&lt;span&gt;args&lt;/span&gt;&lt;span&gt;&lt;span&gt;=&lt;/span&gt;&lt;span&gt;{&lt;/span&gt;&lt;/span&gt;&lt;span&gt;&apos;wait&apos;&lt;/span&gt;&lt;span&gt;: &lt;/span&gt;&lt;span&gt;1&lt;/span&gt;&lt;span&gt;,&lt;/span&gt;&lt;span&gt;&apos;html&apos;&lt;/span&gt;&lt;span&gt;:&lt;/span&gt;&lt;span&gt;1&lt;/span&gt;&lt;span&gt;,&lt;/span&gt;&lt;span&gt;&apos;iframes&apos;&lt;/span&gt;&lt;span&gt;: &lt;/span&gt;&lt;span&gt;1&lt;/span&gt;&lt;span&gt;,&lt;/span&gt;&lt;span&gt;&apos;script&apos;&lt;/span&gt;&lt;span&gt;:&lt;/span&gt;&lt;span&gt;1&lt;/span&gt;&lt;span&gt;})&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;13&lt;/div&gt;&lt;/div&gt;&lt;div&gt;
&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;14&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;    &lt;/span&gt;&lt;span&gt;def&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;parseRequest&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;self&lt;/span&gt;&lt;span&gt;,&lt;/span&gt;&lt;span&gt;response&lt;/span&gt;&lt;span&gt;):&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;15&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;&lt;span&gt;        &lt;/span&gt;&lt;/span&gt;&lt;span&gt;iframe_html &lt;/span&gt;&lt;span&gt;=&lt;/span&gt;&lt;span&gt; response.data[&lt;/span&gt;&lt;span&gt;&apos;childFrames&apos;&lt;/span&gt;&lt;span&gt;][&lt;/span&gt;&lt;span&gt;0&lt;/span&gt;&lt;span&gt;][&lt;/span&gt;&lt;span&gt;&apos;html&apos;&lt;/span&gt;&lt;span&gt;]&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;16&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;&lt;span&gt;        &lt;/span&gt;&lt;/span&gt;&lt;span&gt;sel &lt;/span&gt;&lt;span&gt;=&lt;/span&gt;&lt;span&gt; parsel.&lt;/span&gt;&lt;span&gt;Selector&lt;/span&gt;&lt;span&gt;(iframe_html)&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;17&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;        &lt;/span&gt;&lt;span&gt;# for se in sel.xpath(&apos;//div[@class=&quot;n-rcmd&quot;]/div[@class=&quot;v-hd2&quot;]/div[@class=&quot;tab&quot;]/a&apos;):&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;18&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;        &lt;/span&gt;&lt;span&gt;for&lt;/span&gt;&lt;span&gt; se &lt;/span&gt;&lt;span&gt;in&lt;/span&gt;&lt;span&gt;&lt;span&gt; sel.&lt;/span&gt;&lt;span&gt;xpath&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;/span&gt;&lt;span&gt;&quot;//div[@id=&apos;cateListBox&apos;]/div[@class=&apos;bd&apos;]//a&quot;&lt;/span&gt;&lt;span&gt;)[&lt;/span&gt;&lt;span&gt;1&lt;/span&gt;&lt;span&gt;:]:&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;19&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;&lt;span&gt;            &lt;/span&gt;&lt;/span&gt;&lt;span&gt;item &lt;/span&gt;&lt;span&gt;=&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;Scrapymusic163Item&lt;/span&gt;&lt;span&gt;()&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;20&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;&lt;span&gt;            &lt;/span&gt;&lt;/span&gt;&lt;span&gt;item[&lt;/span&gt;&lt;span&gt;&apos;title&apos;&lt;/span&gt;&lt;span&gt;&lt;span&gt;] &lt;/span&gt;&lt;span&gt;=&lt;/span&gt;&lt;span&gt; se.&lt;/span&gt;&lt;span&gt;xpath&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;/span&gt;&lt;span&gt;&apos;text()&apos;&lt;/span&gt;&lt;span&gt;)[&lt;/span&gt;&lt;span&gt;0&lt;/span&gt;&lt;span&gt;&lt;span&gt;].&lt;/span&gt;&lt;span&gt;extract&lt;/span&gt;&lt;span&gt;()&lt;/span&gt;&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;21&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;&lt;span&gt;            &lt;/span&gt;&lt;/span&gt;&lt;span&gt;item[&lt;/span&gt;&lt;span&gt;&apos;link&apos;&lt;/span&gt;&lt;span&gt;&lt;span&gt;] &lt;/span&gt;&lt;span&gt;=&lt;/span&gt;&lt;span&gt; se.&lt;/span&gt;&lt;span&gt;xpath&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;/span&gt;&lt;span&gt;&apos;@href&apos;&lt;/span&gt;&lt;span&gt;)[&lt;/span&gt;&lt;span&gt;0&lt;/span&gt;&lt;span&gt;&lt;span&gt;].&lt;/span&gt;&lt;span&gt;extract&lt;/span&gt;&lt;span&gt;()&lt;/span&gt;&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;22&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;&lt;span&gt;            &lt;/span&gt;&lt;/span&gt;&lt;span&gt;url &lt;/span&gt;&lt;span&gt;=&lt;/span&gt;&lt;span&gt; response.&lt;/span&gt;&lt;span&gt;urljoin&lt;/span&gt;&lt;span&gt;(item[&lt;/span&gt;&lt;span&gt;&apos;link&apos;&lt;/span&gt;&lt;span&gt;])&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;23&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;            &lt;/span&gt;&lt;span&gt;yield&lt;/span&gt;&lt;span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;SplashRequest&lt;/span&gt;&lt;span&gt;(url,&lt;/span&gt;&lt;/span&gt;&lt;span&gt;self&lt;/span&gt;&lt;span&gt;.parse_gedan,&lt;/span&gt;&lt;span&gt;endpoint&lt;/span&gt;&lt;span&gt;=&lt;/span&gt;&lt;span&gt;&apos;render.json&apos;&lt;/span&gt;&lt;span&gt;, &lt;/span&gt;&lt;span&gt;args&lt;/span&gt;&lt;span&gt;&lt;span&gt;=&lt;/span&gt;&lt;span&gt;{&lt;/span&gt;&lt;/span&gt;&lt;span&gt;&apos;wait&apos;&lt;/span&gt;&lt;span&gt;: &lt;/span&gt;&lt;span&gt;2&lt;/span&gt;&lt;span&gt;,&lt;/span&gt;&lt;span&gt;&apos;html&apos;&lt;/span&gt;&lt;span&gt;:&lt;/span&gt;&lt;span&gt;1&lt;/span&gt;&lt;span&gt;,&lt;/span&gt;&lt;span&gt;&apos;iframes&apos;&lt;/span&gt;&lt;span&gt;: &lt;/span&gt;&lt;span&gt;1&lt;/span&gt;&lt;span&gt;,&lt;/span&gt;&lt;span&gt;&apos;script&apos;&lt;/span&gt;&lt;span&gt;:&lt;/span&gt;&lt;span&gt;1&lt;/span&gt;&lt;span&gt;})&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;24&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;    &lt;/span&gt;&lt;span&gt;def&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;parse_gedan&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;self&lt;/span&gt;&lt;span&gt;,&lt;/span&gt;&lt;span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;response&lt;/span&gt;&lt;/span&gt;&lt;span&gt;):&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;25&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;&lt;span&gt;        &lt;/span&gt;&lt;/span&gt;&lt;span&gt;iframe_html &lt;/span&gt;&lt;span&gt;=&lt;/span&gt;&lt;span&gt; response.data[&lt;/span&gt;&lt;span&gt;&apos;childFrames&apos;&lt;/span&gt;&lt;span&gt;][&lt;/span&gt;&lt;span&gt;0&lt;/span&gt;&lt;span&gt;][&lt;/span&gt;&lt;span&gt;&apos;html&apos;&lt;/span&gt;&lt;span&gt;]&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;26&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;&lt;span&gt;        &lt;/span&gt;&lt;/span&gt;&lt;span&gt;sel &lt;/span&gt;&lt;span&gt;=&lt;/span&gt;&lt;span&gt; parsel.&lt;/span&gt;&lt;span&gt;Selector&lt;/span&gt;&lt;span&gt;(iframe_html)&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;27&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;        &lt;/span&gt;&lt;span&gt;for&lt;/span&gt;&lt;span&gt; se &lt;/span&gt;&lt;span&gt;in&lt;/span&gt;&lt;span&gt;&lt;span&gt; sel.&lt;/span&gt;&lt;span&gt;xpath&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;/span&gt;&lt;span&gt;&apos;//ul[@id=&quot;m-pl-container&quot;]/li&apos;&lt;/span&gt;&lt;span&gt;):&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;28&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;&lt;span&gt;            &lt;/span&gt;&lt;/span&gt;&lt;span&gt;item &lt;/span&gt;&lt;span&gt;=&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;Scrapymusic163Item&lt;/span&gt;&lt;span&gt;()&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;29&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;&lt;span&gt;            &lt;/span&gt;&lt;/span&gt;&lt;span&gt;item[&lt;/span&gt;&lt;span&gt;&apos;title&apos;&lt;/span&gt;&lt;span&gt;&lt;span&gt;] &lt;/span&gt;&lt;span&gt;=&lt;/span&gt;&lt;span&gt; se.&lt;/span&gt;&lt;span&gt;xpath&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;/span&gt;&lt;span&gt;&apos;p[@class=&quot;dec&quot;]/a/text()&apos;&lt;/span&gt;&lt;span&gt;)[&lt;/span&gt;&lt;span&gt;0&lt;/span&gt;&lt;span&gt;&lt;span&gt;].&lt;/span&gt;&lt;span&gt;extract&lt;/span&gt;&lt;span&gt;()&lt;/span&gt;&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;30&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;&lt;span&gt;            &lt;/span&gt;&lt;/span&gt;&lt;span&gt;item[&lt;/span&gt;&lt;span&gt;&apos;link&apos;&lt;/span&gt;&lt;span&gt;&lt;span&gt;] &lt;/span&gt;&lt;span&gt;=&lt;/span&gt;&lt;span&gt; se.&lt;/span&gt;&lt;span&gt;xpath&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;/span&gt;&lt;span&gt;&apos;p[@class=&quot;dec&quot;]/a/@href&apos;&lt;/span&gt;&lt;span&gt;)[&lt;/span&gt;&lt;span&gt;0&lt;/span&gt;&lt;span&gt;&lt;span&gt;].&lt;/span&gt;&lt;span&gt;extract&lt;/span&gt;&lt;span&gt;()&lt;/span&gt;&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;31&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;&lt;span&gt;            &lt;/span&gt;&lt;/span&gt;&lt;span&gt;url&lt;/span&gt;&lt;span&gt;=&lt;/span&gt;&lt;span&gt;response.&lt;/span&gt;&lt;span&gt;urljoin&lt;/span&gt;&lt;span&gt;(item[&lt;/span&gt;&lt;span&gt;&apos;link&apos;&lt;/span&gt;&lt;span&gt;])&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;32&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;&lt;span&gt;            &lt;/span&gt;&lt;/span&gt;&lt;span&gt;item[&lt;/span&gt;&lt;span&gt;&apos;realLink&apos;&lt;/span&gt;&lt;span&gt;&lt;span&gt;]&lt;/span&gt;&lt;span&gt;=&lt;/span&gt;&lt;span&gt;url&lt;/span&gt;&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;33&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;&lt;span&gt;            &lt;/span&gt;&lt;/span&gt;&lt;span&gt;item[&lt;/span&gt;&lt;span&gt;&apos;listenCount&apos;&lt;/span&gt;&lt;span&gt;&lt;span&gt;]&lt;/span&gt;&lt;span&gt;=&lt;/span&gt;&lt;span&gt;se.&lt;/span&gt;&lt;span&gt;xpath&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;/span&gt;&lt;span&gt;&apos;div/div/span[@class=&quot;nb&quot;]/text()&apos;&lt;/span&gt;&lt;span&gt;)[&lt;/span&gt;&lt;span&gt;0&lt;/span&gt;&lt;span&gt;&lt;span&gt;].&lt;/span&gt;&lt;span&gt;extract&lt;/span&gt;&lt;span&gt;()&lt;/span&gt;&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;34&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;            &lt;/span&gt;&lt;span&gt;if&lt;/span&gt;&lt;span&gt;(item[&lt;/span&gt;&lt;span&gt;&apos;listenCount&apos;&lt;/span&gt;&lt;span&gt;] &lt;/span&gt;&lt;span&gt;and&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;&quot;万&quot;&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;in&lt;/span&gt;&lt;span&gt; item[&lt;/span&gt;&lt;span&gt;&apos;listenCount&apos;&lt;/span&gt;&lt;span&gt;]):&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;35&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;&lt;span&gt;                &lt;/span&gt;&lt;/span&gt;&lt;span&gt;item[&lt;/span&gt;&lt;span&gt;&apos;listenCount&apos;&lt;/span&gt;&lt;span&gt;&lt;span&gt;]&lt;/span&gt;&lt;span&gt;=&lt;/span&gt;&lt;/span&gt;&lt;span&gt;int&lt;/span&gt;&lt;span&gt;(item[&lt;/span&gt;&lt;span&gt;&apos;listenCount&apos;&lt;/span&gt;&lt;span&gt;&lt;span&gt;].&lt;/span&gt;&lt;span&gt;replace&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;/span&gt;&lt;span&gt;&quot;万&quot;&lt;/span&gt;&lt;span&gt;,&lt;/span&gt;&lt;span&gt;&quot;0000&quot;&lt;/span&gt;&lt;span&gt;))&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;36&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;&lt;span&gt;            &lt;/span&gt;&lt;/span&gt;&lt;span&gt;item[&lt;/span&gt;&lt;span&gt;&apos;listenCount&apos;&lt;/span&gt;&lt;span&gt;&lt;span&gt;]&lt;/span&gt;&lt;span&gt;=&lt;/span&gt;&lt;/span&gt;&lt;span&gt;int&lt;/span&gt;&lt;span&gt;(item[&lt;/span&gt;&lt;span&gt;&apos;listenCount&apos;&lt;/span&gt;&lt;span&gt;])&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;37&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;&lt;span&gt;            &lt;/span&gt;&lt;/span&gt;&lt;span&gt;item[&lt;/span&gt;&lt;span&gt;&apos;imgUrl&apos;&lt;/span&gt;&lt;span&gt;&lt;span&gt;]&lt;/span&gt;&lt;span&gt;=&lt;/span&gt;&lt;span&gt;se.&lt;/span&gt;&lt;span&gt;xpath&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;/span&gt;&lt;span&gt;&apos;div/img[@class=&quot;j-flag&quot;]/@src&apos;&lt;/span&gt;&lt;span&gt;)[&lt;/span&gt;&lt;span&gt;0&lt;/span&gt;&lt;span&gt;&lt;span&gt;].&lt;/span&gt;&lt;span&gt;extract&lt;/span&gt;&lt;span&gt;()&lt;/span&gt;&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;38&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;            &lt;/span&gt;&lt;span&gt;yield&lt;/span&gt;&lt;span&gt; item&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;39&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;&lt;span&gt;        &lt;/span&gt;&lt;/span&gt;&lt;span&gt;next_pages &lt;/span&gt;&lt;span&gt;=&lt;/span&gt;&lt;span&gt; sel.&lt;/span&gt;&lt;span&gt;xpath&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;&apos;//a[@class=&quot;zbtn znxt&quot;]&apos;&lt;/span&gt;&lt;span&gt;)&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;40&lt;/div&gt;&lt;/div&gt;&lt;div&gt;
&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;41&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;        &lt;/span&gt;&lt;span&gt;if&lt;/span&gt;&lt;span&gt; next_pages:&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;42&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;&lt;span&gt;            &lt;/span&gt;&lt;/span&gt;&lt;span&gt;url &lt;/span&gt;&lt;span&gt;=&lt;/span&gt;&lt;span&gt; response.&lt;/span&gt;&lt;span&gt;urljoin&lt;/span&gt;&lt;span&gt;(next_pages.&lt;/span&gt;&lt;span&gt;xpath&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;&apos;@href&apos;&lt;/span&gt;&lt;span&gt;)[&lt;/span&gt;&lt;span&gt;0&lt;/span&gt;&lt;span&gt;&lt;span&gt;].&lt;/span&gt;&lt;span&gt;extract&lt;/span&gt;&lt;span&gt;())&lt;/span&gt;&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;43&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;            &lt;/span&gt;&lt;span&gt;yield&lt;/span&gt;&lt;span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;SplashRequest&lt;/span&gt;&lt;span&gt;(url,&lt;/span&gt;&lt;/span&gt;&lt;span&gt;self&lt;/span&gt;&lt;span&gt;.parse_gedan,&lt;/span&gt;&lt;span&gt;endpoint&lt;/span&gt;&lt;span&gt;=&lt;/span&gt;&lt;span&gt;&apos;render.json&apos;&lt;/span&gt;&lt;span&gt;, &lt;/span&gt;&lt;span&gt;args&lt;/span&gt;&lt;span&gt;&lt;span&gt;=&lt;/span&gt;&lt;span&gt;{&lt;/span&gt;&lt;/span&gt;&lt;span&gt;&apos;wait&apos;&lt;/span&gt;&lt;span&gt;: &lt;/span&gt;&lt;span&gt;2&lt;/span&gt;&lt;span&gt;,&lt;/span&gt;&lt;span&gt;&apos;html&apos;&lt;/span&gt;&lt;span&gt;:&lt;/span&gt;&lt;span&gt;1&lt;/span&gt;&lt;span&gt;,&lt;/span&gt;&lt;span&gt;&apos;iframes&apos;&lt;/span&gt;&lt;span&gt;: &lt;/span&gt;&lt;span&gt;1&lt;/span&gt;&lt;span&gt;,&lt;/span&gt;&lt;span&gt;&apos;script&apos;&lt;/span&gt;&lt;span&gt;:&lt;/span&gt;&lt;span&gt;1&lt;/span&gt;&lt;span&gt;})&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;44&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;if&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;__name__&lt;/span&gt;&lt;span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;==&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;/span&gt;&lt;span&gt;&apos;__main__&apos;&lt;/span&gt;&lt;span&gt;:&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;45&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;    &lt;/span&gt;&lt;span&gt;pass&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;/code&gt;&lt;/pre&gt;&lt;div&gt;&lt;div&gt;&lt;/div&gt;&lt;div&gt;&lt;/div&gt;&lt;/div&gt;&lt;/figure&gt;&lt;div&gt;&lt;/div&gt;&lt;/div&gt;&lt;span&gt;展开&lt;/span&gt;&lt;span&gt;收起&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;p&gt;上边这段代码是爬取歌单的关键。这里首先说下爬取的思路，打开网易云的网站，找到歌单连接,歌单分为热门和最新两种，每种又根据风格分为72小类，思路就是先爬取所有种类的连接&lt;/p&gt;&lt;p&gt;&lt;/p&gt;&lt;figure&gt;&lt;img alt=&quot;20180515152636458293531.png&quot; loading=&quot;lazy&quot; width=&quot;1386&quot; height=&quot;918&quot; src=&quot;/_astro/20180515152636458293531.Cj5gJs3Q_20XTVH.webp&quot; /&gt;&lt;figcaption&gt;20180515152636458293531.png&lt;/figcaption&gt;&lt;/figure&gt;&lt;p&gt;&lt;/p&gt;&lt;p&gt;再打开每类歌单的网页&lt;/p&gt;&lt;p&gt;&lt;/p&gt;&lt;figure&gt;&lt;img alt=&quot;20180515152636639480608.png&quot; loading=&quot;lazy&quot; width=&quot;1384&quot; height=&quot;1152&quot; src=&quot;/_astro/20180515152636639480608.DqDkJo4D_Z1Wrgc9.webp&quot; /&gt;&lt;figcaption&gt;20180515152636639480608.png&lt;/figcaption&gt;&lt;/figure&gt;&lt;p&gt;&lt;/p&gt;&lt;p&gt;从上面的图片中，我们可以看到每首歌的标题,连接，收听次数等信息。&lt;/p&gt;&lt;/section&gt;
&lt;section&gt;&lt;h3&gt;scrapy-splash&lt;a href=&quot;#scrapy-splash&quot;&gt;&lt;span&gt;#&lt;/span&gt;&lt;/a&gt;&lt;/h3&gt;&lt;p&gt;由于网易云的连接嵌套了动态链接的iframe,这里使用了scrapy-splash来动态渲染iframe，相当于让scrapy-splash来执行网页中的js，并将渲染结果返给我们，这样我们就可以继续使用xpath来筛选自己感兴趣的内容了。&lt;/p&gt;&lt;section&gt;&lt;h4&gt;docker 部署scrapy-splash&lt;a href=&quot;#docker-部署scrapy-splash&quot;&gt;&lt;span&gt;#&lt;/span&gt;&lt;/a&gt;&lt;/h4&gt;&lt;div&gt;&lt;figure&gt;&lt;figcaption&gt;&lt;span&gt;&lt;/span&gt;&lt;span&gt;Terminal window&lt;/span&gt;&lt;/figcaption&gt;&lt;pre&gt;&lt;code&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;1&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;docker&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;pull&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;scrapinghub/splash&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;2&lt;/div&gt;&lt;/div&gt;&lt;div&gt;
&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;3&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;docker&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;run&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;-p&lt;/span&gt;&lt;span&gt;  &lt;/span&gt;&lt;span&gt;8050:8050&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;scrapinghub/splash&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;/code&gt;&lt;/pre&gt;&lt;div&gt;&lt;div&gt;&lt;/div&gt;&lt;div&gt;&lt;/div&gt;&lt;/div&gt;&lt;/figure&gt;&lt;/div&gt;&lt;/section&gt;&lt;section&gt;&lt;h4&gt;配置&lt;a href=&quot;#配置&quot;&gt;&lt;span&gt;#&lt;/span&gt;&lt;/a&gt;&lt;/h4&gt;&lt;div&gt;&lt;figure&gt;&lt;figcaption&gt;&lt;span&gt;&lt;/span&gt;&lt;span&gt;Terminal window&lt;/span&gt;&lt;/figcaption&gt;&lt;pre&gt;&lt;code&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;1&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;vim&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;setting.py&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;/code&gt;&lt;/pre&gt;&lt;div&gt;&lt;div&gt;&lt;/div&gt;&lt;div&gt;&lt;/div&gt;&lt;/div&gt;&lt;/figure&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;&lt;figure&gt;&lt;figcaption&gt;&lt;/figcaption&gt;&lt;pre&gt;&lt;code&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;1&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;#SPLASH SERVER&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;2&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;&lt;span&gt;SPLASH_URL&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;=&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;/span&gt;&lt;span&gt;&apos;http://127.0.0.1:8050/render.json&apos;&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;3&lt;/div&gt;&lt;/div&gt;&lt;div&gt;
&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;4&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;&lt;span&gt;DOWNLOADER_MIDDLEWARES&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;=&lt;/span&gt;&lt;span&gt; {&lt;/span&gt;&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;5&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;    &lt;/span&gt;&lt;span&gt;&apos;scrapy_splash.SplashCookiesMiddleware&apos;&lt;/span&gt;&lt;span&gt;: &lt;/span&gt;&lt;span&gt;723&lt;/span&gt;&lt;span&gt;,&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;6&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;    &lt;/span&gt;&lt;span&gt;&apos;scrapy_splash.SplashMiddleware&apos;&lt;/span&gt;&lt;span&gt;: &lt;/span&gt;&lt;span&gt;725&lt;/span&gt;&lt;span&gt;,&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;7&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;    &lt;/span&gt;&lt;span&gt;&apos;scrapy.downloadermiddlewares.httpcompression.HttpCompressionMiddleware&apos;&lt;/span&gt;&lt;span&gt;: &lt;/span&gt;&lt;span&gt;810&lt;/span&gt;&lt;span&gt;,&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;8&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;}&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;9&lt;/div&gt;&lt;/div&gt;&lt;div&gt;
&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;10&lt;/div&gt;&lt;/div&gt;&lt;div&gt;
&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;11&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;&lt;span&gt;SPIDER_MIDDLEWARES&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;=&lt;/span&gt;&lt;span&gt; {&lt;/span&gt;&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;12&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;    &lt;/span&gt;&lt;span&gt;&apos;scrapy_splash.SplashDeduplicateArgsMiddleware&apos;&lt;/span&gt;&lt;span&gt;: &lt;/span&gt;&lt;span&gt;100&lt;/span&gt;&lt;span&gt;,&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;13&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;}&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;14&lt;/div&gt;&lt;/div&gt;&lt;div&gt;
&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;15&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;&lt;span&gt;DUPEFILTER_CLASS&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;=&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;/span&gt;&lt;span&gt;&apos;scrapy_splash.SplashAwareDupeFilter&apos;&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;16&lt;/div&gt;&lt;/div&gt;&lt;div&gt;
&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;17&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;&lt;span&gt;HTTPCACHE_STORAGE&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;=&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;/span&gt;&lt;span&gt;&apos;scrapy_splash.SplashAwareFSCacheStorage&apos;&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;/code&gt;&lt;/pre&gt;&lt;div&gt;&lt;div&gt;&lt;/div&gt;&lt;div&gt;&lt;/div&gt;&lt;/div&gt;&lt;/figure&gt;&lt;div&gt;&lt;/div&gt;&lt;/div&gt;&lt;span&gt;展开&lt;/span&gt;&lt;span&gt;收起&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;p&gt;详细参见&lt;a href=&quot;http://splash.readthedocs.io/en/latest/&quot; target=&quot;_blank&quot;&gt;scrapy-splash文档&lt;/a&gt;&lt;/p&gt;&lt;p&gt;需要注意的是在使用scrapy-splash时需要用SplashRequest来代替scrapy原始的request,这是scrapy-splash对scrapy的request进行了进一步的封装，支持了更多的参数，同时也支持将request的返回配置为不同的格式，如html,json等。这里使用了json返回。&lt;/p&gt;&lt;/section&gt;&lt;section&gt;&lt;h4&gt;mongodb&lt;a href=&quot;#mongodb&quot;&gt;&lt;span&gt;#&lt;/span&gt;&lt;/a&gt;&lt;/h4&gt;&lt;p&gt;依赖&lt;a href=&quot;https://github.com/sebdah/scrapy-mongodb&quot; target=&quot;_blank&quot;&gt;scrapy-mongodb&lt;/a&gt;&lt;/p&gt;&lt;div&gt;&lt;figure&gt;&lt;figcaption&gt;&lt;/figcaption&gt;&lt;pre&gt;&lt;code&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;1&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;&lt;span&gt;ITEM_PIPELINES&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;=&lt;/span&gt;&lt;span&gt; {&lt;/span&gt;&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;2&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;  &lt;/span&gt;&lt;span&gt;&quot;scrapy_mongodb.MongoDBPipeline&quot;&lt;/span&gt;&lt;span&gt;:&lt;/span&gt;&lt;span&gt;900&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;3&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;}&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;4&lt;/div&gt;&lt;/div&gt;&lt;div&gt;
&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;5&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;&lt;span&gt;MONGODB_URI&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;=&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;/span&gt;&lt;span&gt;&apos;mongodb://root:&apos;&lt;/span&gt;&lt;span&gt;&lt;span&gt;+&lt;/span&gt;&lt;span&gt;urllib.parse.&lt;/span&gt;&lt;span&gt;quote&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;/span&gt;&lt;span&gt;&apos;MyNewPass+@321&apos;&lt;/span&gt;&lt;span&gt;&lt;span&gt;)&lt;/span&gt;&lt;span&gt;+&lt;/span&gt;&lt;/span&gt;&lt;span&gt;&apos;@localhost:27017&apos;&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;6&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;&lt;span&gt;MONGODB_DATABASE&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;=&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;/span&gt;&lt;span&gt;&apos;scrapy&apos;&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;7&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;# MONGODB_COLLECTION = &apos;my_items&apos;&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;8&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;# 连接不重复，避免重复爬取&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;9&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;&lt;span&gt;MONGODB_UNIQUE_KEY&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;=&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;/span&gt;&lt;span&gt;&apos;realLink&apos;&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;10&lt;/div&gt;&lt;/div&gt;&lt;div&gt;
&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;11&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;&lt;span&gt;MONGODB_SEPARATE_COLLECTIONS&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;=&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;/span&gt;&lt;span&gt;True&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;/code&gt;&lt;/pre&gt;&lt;div&gt;&lt;div&gt;&lt;/div&gt;&lt;div&gt;&lt;/div&gt;&lt;/div&gt;&lt;/figure&gt;&lt;/div&gt;&lt;/section&gt;&lt;/section&gt;
&lt;section&gt;&lt;h3&gt;爬取结果&lt;a href=&quot;#爬取结果&quot;&gt;&lt;span&gt;#&lt;/span&gt;&lt;/a&gt;&lt;/h3&gt;&lt;p&gt;&lt;/p&gt;&lt;figure&gt;&lt;img alt=&quot;20180515152636644617456.png&quot; loading=&quot;lazy&quot; width=&quot;1266&quot; height=&quot;260&quot; src=&quot;/_astro/20180515152636644617456.BjqZx949_XUOMt.webp&quot; /&gt;&lt;figcaption&gt;20180515152636644617456.png&lt;/figcaption&gt;&lt;/figure&gt;&lt;p&gt;&lt;/p&gt;&lt;p&gt;共爬取了15万左右的歌单。下面是播放次数最多的五个歌单&lt;/p&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;&lt;figure&gt;&lt;figcaption&gt;&lt;/figcaption&gt;&lt;pre&gt;&lt;code&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;1&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;/* 1 */&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;2&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;{&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;3&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;    &lt;/span&gt;&lt;span&gt;&quot;_id&quot;&lt;/span&gt;&lt;span&gt; : &lt;/span&gt;&lt;span&gt;ObjectId&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;&quot;5aea7a036d5b76bf8d42659d&quot;&lt;/span&gt;&lt;span&gt;),&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;4&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;    &lt;/span&gt;&lt;span&gt;&quot;title&quot;&lt;/span&gt;&lt;span&gt; : &lt;/span&gt;&lt;span&gt;&quot;华语速爆新歌&quot;&lt;/span&gt;&lt;span&gt;,&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;5&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;    &lt;/span&gt;&lt;span&gt;&quot;link&quot;&lt;/span&gt;&lt;span&gt; : &lt;/span&gt;&lt;span&gt;&quot;/playlist?id=924680166&quot;&lt;/span&gt;&lt;span&gt;,&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;6&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;    &lt;/span&gt;&lt;span&gt;&quot;realLink&quot;&lt;/span&gt;&lt;span&gt; : &lt;/span&gt;&lt;span&gt;&quot;https://music.163.com/playlist?id=924680166&quot;&lt;/span&gt;&lt;span&gt;,&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;7&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;    &lt;/span&gt;&lt;span&gt;&quot;listenCount&quot;&lt;/span&gt;&lt;span&gt; : &lt;/span&gt;&lt;span&gt;152900000&lt;/span&gt;&lt;span&gt;,&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;8&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;    &lt;/span&gt;&lt;span&gt;&quot;imgUrl&quot;&lt;/span&gt;&lt;span&gt; : &lt;/span&gt;&lt;span&gt;&quot;http://p1.music.126.net/Zcv4NgK5T0bd1yMzAFp8Iw==/109951163276364917.jpg?param=140y140&quot;&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;9&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;}&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;10&lt;/div&gt;&lt;/div&gt;&lt;div&gt;
&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;11&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;/* 2 */&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;12&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;{&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;13&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;    &lt;/span&gt;&lt;span&gt;&quot;_id&quot;&lt;/span&gt;&lt;span&gt; : &lt;/span&gt;&lt;span&gt;ObjectId&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;&quot;5aea78a56d5b76bf8d422179&quot;&lt;/span&gt;&lt;span&gt;),&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;14&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;    &lt;/span&gt;&lt;span&gt;&quot;title&quot;&lt;/span&gt;&lt;span&gt; : &lt;/span&gt;&lt;span&gt;&quot;【节奏控】那些超带感的音乐（典藏版）&quot;&lt;/span&gt;&lt;span&gt;,&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;15&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;    &lt;/span&gt;&lt;span&gt;&quot;link&quot;&lt;/span&gt;&lt;span&gt; : &lt;/span&gt;&lt;span&gt;&quot;/playlist?id=306397077&quot;&lt;/span&gt;&lt;span&gt;,&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;16&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;    &lt;/span&gt;&lt;span&gt;&quot;realLink&quot;&lt;/span&gt;&lt;span&gt; : &lt;/span&gt;&lt;span&gt;&quot;https://music.163.com/playlist?id=306397077&quot;&lt;/span&gt;&lt;span&gt;,&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;17&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;    &lt;/span&gt;&lt;span&gt;&quot;listenCount&quot;&lt;/span&gt;&lt;span&gt; : &lt;/span&gt;&lt;span&gt;140930000&lt;/span&gt;&lt;span&gt;,&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;18&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;    &lt;/span&gt;&lt;span&gt;&quot;imgUrl&quot;&lt;/span&gt;&lt;span&gt; : &lt;/span&gt;&lt;span&gt;&quot;http://p1.music.126.net/RnOZHM0BNxXuy-RwQQI5BA==/3313928048221849.jpg?param=140y140&quot;&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;19&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;}&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;20&lt;/div&gt;&lt;/div&gt;&lt;div&gt;
&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;21&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;/* 3 */&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;22&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;{&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;23&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;    &lt;/span&gt;&lt;span&gt;&quot;_id&quot;&lt;/span&gt;&lt;span&gt; : &lt;/span&gt;&lt;span&gt;ObjectId&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;&quot;5aea7fd16d5b76bf8d43b0db&quot;&lt;/span&gt;&lt;span&gt;),&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;24&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;    &lt;/span&gt;&lt;span&gt;&quot;title&quot;&lt;/span&gt;&lt;span&gt; : &lt;/span&gt;&lt;span&gt;&quot;【旋律控】超级好听的外文歌&quot;&lt;/span&gt;&lt;span&gt;,&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;25&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;    &lt;/span&gt;&lt;span&gt;&quot;link&quot;&lt;/span&gt;&lt;span&gt; : &lt;/span&gt;&lt;span&gt;&quot;/playlist?id=310970433&quot;&lt;/span&gt;&lt;span&gt;,&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;26&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;    &lt;/span&gt;&lt;span&gt;&quot;realLink&quot;&lt;/span&gt;&lt;span&gt; : &lt;/span&gt;&lt;span&gt;&quot;https://music.163.com/playlist?id=310970433&quot;&lt;/span&gt;&lt;span&gt;,&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;27&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;    &lt;/span&gt;&lt;span&gt;&quot;listenCount&quot;&lt;/span&gt;&lt;span&gt; : &lt;/span&gt;&lt;span&gt;101740000&lt;/span&gt;&lt;span&gt;,&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;28&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;    &lt;/span&gt;&lt;span&gt;&quot;imgUrl&quot;&lt;/span&gt;&lt;span&gt; : &lt;/span&gt;&lt;span&gt;&quot;http://p1.music.126.net/2MsstS-M9w5-li0aRy3sUQ==/1380986606815861.jpg?param=140y140&quot;&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;29&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;}&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;30&lt;/div&gt;&lt;/div&gt;&lt;div&gt;
&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;31&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;/* 4 */&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;32&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;{&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;33&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;    &lt;/span&gt;&lt;span&gt;&quot;_id&quot;&lt;/span&gt;&lt;span&gt; : &lt;/span&gt;&lt;span&gt;ObjectId&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;&quot;5aea95f76d5b76bf8d46289a&quot;&lt;/span&gt;&lt;span&gt;),&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;34&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;    &lt;/span&gt;&lt;span&gt;&quot;title&quot;&lt;/span&gt;&lt;span&gt; : &lt;/span&gt;&lt;span&gt;&quot;那些只听前奏就中毒的英文歌&quot;&lt;/span&gt;&lt;span&gt;,&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;35&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;    &lt;/span&gt;&lt;span&gt;&quot;link&quot;&lt;/span&gt;&lt;span&gt; : &lt;/span&gt;&lt;span&gt;&quot;/playlist?id=37432514&quot;&lt;/span&gt;&lt;span&gt;,&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;36&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;    &lt;/span&gt;&lt;span&gt;&quot;realLink&quot;&lt;/span&gt;&lt;span&gt; : &lt;/span&gt;&lt;span&gt;&quot;https://music.163.com/playlist?id=37432514&quot;&lt;/span&gt;&lt;span&gt;,&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;37&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;    &lt;/span&gt;&lt;span&gt;&quot;listenCount&quot;&lt;/span&gt;&lt;span&gt; : &lt;/span&gt;&lt;span&gt;95420000&lt;/span&gt;&lt;span&gt;,&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;38&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;    &lt;/span&gt;&lt;span&gt;&quot;imgUrl&quot;&lt;/span&gt;&lt;span&gt; : &lt;/span&gt;&lt;span&gt;&quot;http://p1.music.126.net/mQy3lRj6YJ0TW3fM9v85YA==/6643249256145165.jpg?param=140y140&quot;&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;39&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;}&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;40&lt;/div&gt;&lt;/div&gt;&lt;div&gt;
&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;41&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;/* 5 */&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;42&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;{&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;43&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;    &lt;/span&gt;&lt;span&gt;&quot;_id&quot;&lt;/span&gt;&lt;span&gt; : &lt;/span&gt;&lt;span&gt;ObjectId&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;&quot;5aea789a6d5b76bf8d42194c&quot;&lt;/span&gt;&lt;span&gt;),&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;44&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;    &lt;/span&gt;&lt;span&gt;&quot;title&quot;&lt;/span&gt;&lt;span&gt; : &lt;/span&gt;&lt;span&gt;&quot;【欧美男团】秒杀耳朵系列&quot;&lt;/span&gt;&lt;span&gt;,&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;45&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;    &lt;/span&gt;&lt;span&gt;&quot;link&quot;&lt;/span&gt;&lt;span&gt; : &lt;/span&gt;&lt;span&gt;&quot;/playlist?id=117377955&quot;&lt;/span&gt;&lt;span&gt;,&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;46&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;    &lt;/span&gt;&lt;span&gt;&quot;realLink&quot;&lt;/span&gt;&lt;span&gt; : &lt;/span&gt;&lt;span&gt;&quot;https://music.163.com/playlist?id=117377955&quot;&lt;/span&gt;&lt;span&gt;,&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;47&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;    &lt;/span&gt;&lt;span&gt;&quot;listenCount&quot;&lt;/span&gt;&lt;span&gt; : &lt;/span&gt;&lt;span&gt;71150000&lt;/span&gt;&lt;span&gt;,&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;48&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;    &lt;/span&gt;&lt;span&gt;&quot;imgUrl&quot;&lt;/span&gt;&lt;span&gt; : &lt;/span&gt;&lt;span&gt;&quot;http://p1.music.126.net/FJAxNkFoq3dGiS9tz_bGgQ==/3405187512421439.jpg?param=140y140&quot;&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;49&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;}&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;/code&gt;&lt;/pre&gt;&lt;div&gt;&lt;div&gt;&lt;/div&gt;&lt;div&gt;&lt;/div&gt;&lt;/div&gt;&lt;/figure&gt;&lt;div&gt;&lt;/div&gt;&lt;/div&gt;&lt;span&gt;展开&lt;/span&gt;&lt;span&gt;收起&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;/section&gt;
&lt;section&gt;&lt;h3&gt;github地址&lt;a href=&quot;#github地址&quot;&gt;&lt;span&gt;#&lt;/span&gt;&lt;/a&gt;&lt;/h3&gt;&lt;p&gt;&lt;a href=&quot;https://github.com/tracerZzz/music163Scrapy&quot; target=&quot;_blank&quot;&gt;https://github.com/tracerZzz/music163Scrapy&lt;/a&gt;&lt;/p&gt;&lt;/section&gt;
&lt;section&gt;&lt;h3&gt;参考&lt;a href=&quot;#参考&quot;&gt;&lt;span&gt;#&lt;/span&gt;&lt;/a&gt;&lt;/h3&gt;&lt;p&gt;&lt;a href=&quot;http://scrapy-chs.readthedocs.io/zh_CN/latest/intro/tutorial.html&quot; target=&quot;_blank&quot;&gt;scrapy文档&lt;/a&gt;&lt;/p&gt;&lt;p&gt;&lt;a href=&quot;http://splash.readthedocs.io/en/latest/&quot; target=&quot;_blank&quot;&gt;scrapy-splash文档&lt;/a&gt;&lt;/p&gt;&lt;p&gt;&lt;a href=&quot;https://github.com/sebdah/scrapy-mongodb&quot; target=&quot;_blank&quot;&gt;scrapy-mongodb&lt;/a&gt;&lt;/p&gt;&lt;/section&gt;</content:encoded></item><item><title>Keepalived + MySQL 双主：搭建 Zabbix Proxy 高可用集群</title><link>https://tracerzzz.cn/posts/keepalived-mysql-dual-master/</link><guid isPermaLink="true">https://tracerzzz.cn/posts/keepalived-mysql-dual-master/</guid><description>2018 年在中国电信 CDN 部门为上千台机器的监控系统搭建的 Zabbix Proxy 高可用方案：MySQL 双主互备加 Keepalived 虚拟 IP 漂移。包含完整搭建步骤、故障切换演练方法，以及双主模式里容易踩的坑和现在回头看的改进方向。</description><pubDate>Fri, 23 Mar 2018 04:37:11 GMT</pubDate><content:encoded>&lt;blockquote&gt;&lt;p&gt;&lt;strong&gt;关于这篇文章&lt;/strong&gt;：搭建步骤写于 2018 年，基于当时的 CentOS 7、MySQL 5.7、Zabbix 3.4。2026 年 10 月补充了背景和架构图，新增了「故障切换演练」「双主模式容易踩的坑」「现在回头看」三节，并对文中的 IP 和密码做了脱敏处理。&lt;/p&gt;&lt;/blockquote&gt;
&lt;section&gt;&lt;h3&gt;背景&lt;a href=&quot;#背景&quot;&gt;&lt;span&gt;#&lt;/span&gt;&lt;/a&gt;&lt;/h3&gt;&lt;p&gt;2018 年我在中国电信云计算 CDN 部门，参与搭建天翼高清业务的监控系统，覆盖内容中心和各省中心约一千台机器。监控采用 Zabbix 的 server - proxy - agent 三层架构：agent 把数据上报给所在区域的 proxy，proxy 汇总后再发给 server。&lt;/p&gt;&lt;p&gt;proxy 是中间这一层的单点：一台 proxy 挂了，它下面整片机器的监控数据就断了。所以需要给 proxy 做高可用，方案是：&lt;/p&gt;&lt;ul&gt;
&lt;li&gt;两台机器各跑一套 zabbix-proxy 和 MySQL；&lt;/li&gt;
&lt;li&gt;两个 MySQL &lt;strong&gt;双主互备&lt;/strong&gt;，任意一台写入的数据都会同步到另一台；&lt;/li&gt;
&lt;li&gt;Keepalived 维护一个&lt;strong&gt;虚拟 IP（VIP）&lt;/strong&gt;，agent 和 server 都只认这个 VIP，主机故障时 VIP 自动漂移到备机。&lt;/li&gt;
&lt;/ul&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;&lt;p&gt;节点 B（slave）&lt;/p&gt;&lt;/span&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;&lt;p&gt;节点 A（master）&lt;/p&gt;&lt;/span&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;&lt;/span&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;&lt;/span&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;&lt;/span&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;&lt;p&gt;双主互相同步 binlog&lt;/p&gt;&lt;/span&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;&lt;p&gt;VRRP 心跳&lt;/p&gt;&lt;/span&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;&lt;/span&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;&lt;/span&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;&lt;p&gt;正常时&lt;/p&gt;&lt;/span&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;&lt;p&gt;故障切换后&lt;/p&gt;&lt;/span&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;&lt;p&gt;Zabbix Agent（约千台机器）&lt;/p&gt;&lt;/span&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;&lt;p&gt;VIP 192.168.10.100&lt;/p&gt;&lt;/span&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;&lt;p&gt;zabbix-proxy&lt;/p&gt;&lt;/span&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;&lt;p&gt;MySQL&lt;/p&gt;&lt;/span&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;&lt;p&gt;keepalived&lt;/p&gt;&lt;/span&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;&lt;p&gt;zabbix-proxy&lt;/p&gt;&lt;/span&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;&lt;p&gt;MySQL&lt;/p&gt;&lt;/span&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;&lt;p&gt;keepalived&lt;/p&gt;&lt;/span&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;&lt;p&gt;Zabbix Server&lt;/p&gt;&lt;/span&gt;&lt;/div&gt;
&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;&lt;p&gt;节点 B（slave）&lt;/p&gt;&lt;/span&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;&lt;p&gt;节点 A（master）&lt;/p&gt;&lt;/span&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;&lt;/span&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;&lt;/span&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;&lt;/span&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;&lt;p&gt;双主互相同步 binlog&lt;/p&gt;&lt;/span&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;&lt;p&gt;VRRP 心跳&lt;/p&gt;&lt;/span&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;&lt;/span&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;&lt;/span&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;&lt;p&gt;正常时&lt;/p&gt;&lt;/span&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;&lt;p&gt;故障切换后&lt;/p&gt;&lt;/span&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;&lt;p&gt;Zabbix Agent（约千台机器）&lt;/p&gt;&lt;/span&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;&lt;p&gt;VIP 192.168.10.100&lt;/p&gt;&lt;/span&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;&lt;p&gt;zabbix-proxy&lt;/p&gt;&lt;/span&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;&lt;p&gt;MySQL&lt;/p&gt;&lt;/span&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;&lt;p&gt;keepalived&lt;/p&gt;&lt;/span&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;&lt;p&gt;zabbix-proxy&lt;/p&gt;&lt;/span&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;&lt;p&gt;MySQL&lt;/p&gt;&lt;/span&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;&lt;p&gt;keepalived&lt;/p&gt;&lt;/span&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;&lt;p&gt;Zabbix Server&lt;/p&gt;&lt;/span&gt;&lt;/div&gt;
&lt;/div&gt;&lt;/div&gt;&lt;/div&gt;&lt;p&gt;软件版本：CentOS 7、MySQL 5.7、Zabbix 3.4（2018 年）。下面先讲两个核心组件，再按步骤搭建。&lt;/p&gt;&lt;/section&gt;
&lt;section&gt;&lt;h3&gt;Mysql双主模式&lt;a href=&quot;#mysql双主模式&quot;&gt;&lt;span&gt;#&lt;/span&gt;&lt;/a&gt;&lt;/h3&gt;&lt;p&gt;mysql双主模式也叫做主主同步或者双主互备，即两台数据库server互为主备。双主模式是在普通的master-slave主备模式下进一步拓展而来的，普通的master-slave主备模式即一主一备，slave主机拉取master主机的Binary log，然后将二进制文件解析为sql语句并完全顺序的执行sql语句所记录的所有操作，从而使slave的数据和master保持一致；双主互备是两台机器互相以对方为master，定时拉取对方的Binary log。双主互备中两台主机都可以进行读写操作，而在普通主备，只有master可以进行写操作，slave只能进行读取操作；&lt;/p&gt;&lt;p&gt;&lt;/p&gt;&lt;figure&gt;&lt;img alt=&quot;20180316152118561067778.png&quot; loading=&quot;lazy&quot; width=&quot;1574&quot; height=&quot;1324&quot; src=&quot;/_astro/20180316152118561067778.0-1mlIX5_JXaoE.webp&quot; /&gt;&lt;figcaption&gt;20180316152118561067778.png&lt;/figcaption&gt;&lt;/figure&gt;&lt;p&gt;&lt;/p&gt;&lt;hr /&gt;&lt;/section&gt;
&lt;section&gt;&lt;h3&gt;Keepalived&lt;a href=&quot;#keepalived&quot;&gt;&lt;span&gt;#&lt;/span&gt;&lt;/a&gt;&lt;/h3&gt;&lt;p&gt;&lt;a href=&quot;http://www.keepalived.org/doc/&quot; target=&quot;_blank&quot;&gt;Keepalived&lt;/a&gt;是一款高可用软件。keepalived可以让服务器集群共享一个虚拟ip，也就是我们说的vip，同一时间只有一个服务器占有这个虚拟ip，若该服务器不可用或者该服务器的权重小于其他机器，则虚拟IP漂移至另一台服务器并对外提供服务；&lt;/p&gt;&lt;/section&gt;
&lt;section&gt;&lt;h3&gt;准备工作&lt;a href=&quot;#准备工作&quot;&gt;&lt;span&gt;#&lt;/span&gt;&lt;/a&gt;&lt;/h3&gt;&lt;section&gt;&lt;h4&gt;机器准备（centos7系统）&lt;a href=&quot;#机器准备centos7系统&quot;&gt;&lt;span&gt;#&lt;/span&gt;&lt;/a&gt;&lt;/h4&gt;&lt;p&gt;两台 server：&lt;code&gt;192.168.10.11&lt;/code&gt;（&lt;strong&gt;master&lt;/strong&gt;）、&lt;code&gt;192.168.10.12&lt;/code&gt;（&lt;strong&gt;slave&lt;/strong&gt;），一个 VIP：&lt;code&gt;192.168.10.100&lt;/code&gt;（文中 IP 和密码均为示例）&lt;/p&gt;&lt;blockquote&gt;&lt;p&gt;由于基于vrrp协议，两台server和vip必须处于同一网段&lt;/p&gt;&lt;/blockquote&gt;&lt;/section&gt;&lt;section&gt;&lt;h4&gt;安装mysql 并初始化密码&lt;a href=&quot;#安装mysql-并初始化密码&quot;&gt;&lt;span&gt;#&lt;/span&gt;&lt;/a&gt;&lt;/h4&gt;&lt;p&gt;首先登陆master&lt;/p&gt;&lt;div&gt;&lt;figure&gt;&lt;figcaption&gt;&lt;span&gt;&lt;/span&gt;&lt;span&gt;Terminal window&lt;/span&gt;&lt;/figcaption&gt;&lt;pre&gt;&lt;code&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;1&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;yum&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;install&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;mysql-server&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;2&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;mysql&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;-h&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;localhost&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;-u&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;root&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;-p&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;/code&gt;&lt;/pre&gt;&lt;div&gt;&lt;div&gt;&lt;/div&gt;&lt;div&gt;&lt;/div&gt;&lt;/div&gt;&lt;/figure&gt;&lt;/div&gt;&lt;p&gt;发现需要输入密码,于是去日志文件里找默认密码&lt;/p&gt;&lt;div&gt;&lt;figure&gt;&lt;figcaption&gt;&lt;span&gt;&lt;/span&gt;&lt;span&gt;Terminal window&lt;/span&gt;&lt;/figcaption&gt;&lt;pre&gt;&lt;code&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;1&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;vim&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;/var/log/mysql.log&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;/code&gt;&lt;/pre&gt;&lt;div&gt;&lt;div&gt;&lt;/div&gt;&lt;div&gt;&lt;/div&gt;&lt;/div&gt;&lt;/figure&gt;&lt;/div&gt;&lt;p&gt;找到下面一行&lt;/p&gt;&lt;p&gt;&lt;code&gt;[Note] A temporary password is generated for root@localhost: &amp;lt;临时密码&amp;gt;&lt;/code&gt;&lt;/p&gt;&lt;p&gt;&lt;code&gt;&amp;lt;临时密码&amp;gt;&lt;/code&gt;是初始化的密码，再次使用&lt;code&gt;mysql -h localhost -u root -p&lt;/code&gt;登陆&lt;/p&gt;&lt;p&gt;使用以下命令重置root密码&lt;/p&gt;&lt;div&gt;&lt;figure&gt;&lt;figcaption&gt;&lt;/figcaption&gt;&lt;pre&gt;&lt;code&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;1&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;ALTER&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;USER&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;&apos;root&apos;&lt;/span&gt;&lt;span&gt;@&lt;/span&gt;&lt;span&gt;&apos;localhost&apos;&lt;/span&gt;&lt;span&gt; IDENTIFIED &lt;/span&gt;&lt;span&gt;BY&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;&apos;&amp;lt;your-password&amp;gt;&apos;&lt;/span&gt;&lt;span&gt;;&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;/code&gt;&lt;/pre&gt;&lt;div&gt;&lt;div&gt;&lt;/div&gt;&lt;div&gt;&lt;/div&gt;&lt;/div&gt;&lt;/figure&gt;&lt;/div&gt;&lt;p&gt;登陆slave执行相同的操作&lt;/p&gt;&lt;/section&gt;&lt;/section&gt;
&lt;section&gt;&lt;h3&gt;下载zabbix-proxy，导入表结构&lt;a href=&quot;#下载zabbix-proxy导入表结构&quot;&gt;&lt;span&gt;#&lt;/span&gt;&lt;/a&gt;&lt;/h3&gt;&lt;div&gt;&lt;figure&gt;&lt;figcaption&gt;&lt;span&gt;&lt;/span&gt;&lt;span&gt;Terminal window&lt;/span&gt;&lt;/figcaption&gt;&lt;pre&gt;&lt;code&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;1&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;yum&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;install&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;zabbix-proxy-mysql&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;/code&gt;&lt;/pre&gt;&lt;div&gt;&lt;div&gt;&lt;/div&gt;&lt;div&gt;&lt;/div&gt;&lt;/div&gt;&lt;/figure&gt;&lt;/div&gt;&lt;p&gt;然后根据&lt;a href=&quot;https://www.zabbix.com/documentation/3.4/manual/installation/install_from_packages/rhel_centos&quot; target=&quot;_blank&quot;&gt;zabbix文档&lt;/a&gt;提示创建数据库表结构&lt;/p&gt;&lt;div&gt;&lt;figure&gt;&lt;figcaption&gt;&lt;span&gt;&lt;/span&gt;&lt;span&gt;Terminal window&lt;/span&gt;&lt;/figcaption&gt;&lt;pre&gt;&lt;code&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;1&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;mysql&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;-h&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;localhost&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;-u&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;root&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;-p&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;/code&gt;&lt;/pre&gt;&lt;div&gt;&lt;div&gt;&lt;/div&gt;&lt;div&gt;&lt;/div&gt;&lt;/div&gt;&lt;/figure&gt;&lt;/div&gt;&lt;div&gt;&lt;figure&gt;&lt;figcaption&gt;&lt;/figcaption&gt;&lt;pre&gt;&lt;code&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;1&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;create&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;database&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;zabbix_proxy&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;character&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;set&lt;/span&gt;&lt;span&gt; utf8 &lt;/span&gt;&lt;span&gt;collate&lt;/span&gt;&lt;span&gt; utf8_bin;&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;/code&gt;&lt;/pre&gt;&lt;div&gt;&lt;div&gt;&lt;/div&gt;&lt;div&gt;&lt;/div&gt;&lt;/div&gt;&lt;/figure&gt;&lt;/div&gt;&lt;p&gt;为zabbix_proxy库创建zabbix用户并授权，然后退出（避免使用root登陆特定的数据库）&lt;/p&gt;&lt;div&gt;&lt;figure&gt;&lt;figcaption&gt;&lt;/figcaption&gt;&lt;pre&gt;&lt;code&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;1&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;grant&lt;/span&gt;&lt;span&gt; all privileges &lt;/span&gt;&lt;span&gt;on&lt;/span&gt;&lt;span&gt; zabbix_proxy.* &lt;/span&gt;&lt;span&gt;to&lt;/span&gt;&lt;span&gt;&lt;span&gt; zabbix&lt;/span&gt;&lt;span&gt;@localhost&lt;/span&gt;&lt;span&gt; identified &lt;/span&gt;&lt;/span&gt;&lt;span&gt;by&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;&apos;&amp;lt;zabbix-db-password&amp;gt;&apos;&lt;/span&gt;&lt;span&gt;;&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;2&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;quit&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;/code&gt;&lt;/pre&gt;&lt;div&gt;&lt;div&gt;&lt;/div&gt;&lt;div&gt;&lt;/div&gt;&lt;/div&gt;&lt;/figure&gt;&lt;/div&gt;&lt;p&gt;执行zabbix_proxy自带的sql生成zabbix_proxy表结构&lt;/p&gt;&lt;div&gt;&lt;figure&gt;&lt;figcaption&gt;&lt;span&gt;&lt;/span&gt;&lt;span&gt;Terminal window&lt;/span&gt;&lt;/figcaption&gt;&lt;pre&gt;&lt;code&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;1&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;zcat&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;/usr/share/doc/zabbix-proxy-mysql-3.4.1/schema.sql.gz&lt;/span&gt;&lt;span&gt; |&lt;/span&gt;&lt;span&gt;mysql&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;-uroot&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;-p&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;zabbix_proxy&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;/code&gt;&lt;/pre&gt;&lt;div&gt;&lt;div&gt;&lt;/div&gt;&lt;div&gt;&lt;/div&gt;&lt;/div&gt;&lt;/figure&gt;&lt;/div&gt;&lt;p&gt;这个过程需要耐心等待一会，然后登陆数据库查看表数量 一共140个，如果数量不对，需要drop掉zabbix_proxy重新再生成一次。&lt;/p&gt;&lt;p&gt;最后在slave机器上也执行以上操作。确保两台机器的数据库表结构一致；&lt;/p&gt;&lt;/section&gt;
&lt;section&gt;&lt;h3&gt;修改/etc/my.cnf文件，配置同步&lt;a href=&quot;#修改etcmycnf文件配置同步&quot;&gt;&lt;span&gt;#&lt;/span&gt;&lt;/a&gt;&lt;/h3&gt;&lt;div&gt;&lt;figure&gt;&lt;figcaption&gt;&lt;span&gt;&lt;/span&gt;&lt;span&gt;Terminal window&lt;/span&gt;&lt;/figcaption&gt;&lt;pre&gt;&lt;code&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;1&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;vim&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;/etc/my.cnf&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;/code&gt;&lt;/pre&gt;&lt;div&gt;&lt;div&gt;&lt;/div&gt;&lt;div&gt;&lt;/div&gt;&lt;/div&gt;&lt;/figure&gt;&lt;/div&gt;&lt;p&gt;在[mysqld]下追加&lt;/p&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;&lt;figure&gt;&lt;figcaption&gt;&lt;/figcaption&gt;&lt;pre&gt;&lt;code&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;1&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;log-bin&lt;/span&gt;&lt;span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;=&lt;/span&gt;&lt;span&gt; mysql-bin    &lt;/span&gt;&lt;/span&gt;&lt;span&gt;#开启binlog日志&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;2&lt;/div&gt;&lt;/div&gt;&lt;div&gt;
&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;3&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;binlog_format&lt;/span&gt;&lt;span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;=&lt;/span&gt;&lt;span&gt; mixed  &lt;/span&gt;&lt;/span&gt;&lt;span&gt;#基于混合模式,mysql Replication 有三种模式，基于语句，基于行，基于混合模式&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;4&lt;/div&gt;&lt;/div&gt;&lt;div&gt;
&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;5&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;server-id&lt;/span&gt;&lt;span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;=&lt;/span&gt;&lt;span&gt; 1          &lt;/span&gt;&lt;/span&gt;&lt;span&gt;#server-id master要和slave区分开，master为1 ，slave为2&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;6&lt;/div&gt;&lt;/div&gt;&lt;div&gt;
&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;7&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;relay-log&lt;/span&gt;&lt;span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;=&lt;/span&gt;&lt;span&gt; relay-bin  &lt;/span&gt;&lt;/span&gt;&lt;span&gt;# 开启中继日志&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;8&lt;/div&gt;&lt;/div&gt;&lt;div&gt;
&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;9&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;relay-log-index&lt;/span&gt;&lt;span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;=&lt;/span&gt;&lt;span&gt; slave-relay-bin.index &lt;/span&gt;&lt;/span&gt;&lt;span&gt;#中继日志的索引文件&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;10&lt;/div&gt;&lt;/div&gt;&lt;div&gt;
&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;11&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;auto-increment-increment&lt;/span&gt;&lt;span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;=&lt;/span&gt;&lt;span&gt; 2 &lt;/span&gt;&lt;/span&gt;&lt;span&gt;#主键自增的幅度，这个要和机器的数量一致&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;12&lt;/div&gt;&lt;/div&gt;&lt;div&gt;
&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;13&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;auto-increment-offset&lt;/span&gt;&lt;span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;=&lt;/span&gt;&lt;span&gt; 1  &lt;/span&gt;&lt;/span&gt;&lt;span&gt;#从几开始自增，master从1开始自增，每次增加2,即master生成的主键永远为奇数&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;14&lt;/div&gt;&lt;/div&gt;&lt;div&gt;
&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;15&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;replicate-wild-do-table&lt;/span&gt;&lt;span&gt;&lt;span&gt;=&lt;/span&gt;&lt;span&gt;zabbix_proxy.% &lt;/span&gt;&lt;/span&gt;&lt;span&gt;#需要同步的表，这里是zabbix_proxy下的所有的表,用通配符%表示&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;16&lt;/div&gt;&lt;/div&gt;&lt;div&gt;
&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;17&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;slave_skip_errors&lt;/span&gt;&lt;span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;=&lt;/span&gt;&lt;span&gt; 1062,1032,1060  &lt;/span&gt;&lt;/span&gt;&lt;span&gt;#同步时跳过的错误编号，1062，1032，1060&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;/code&gt;&lt;/pre&gt;&lt;div&gt;&lt;div&gt;&lt;/div&gt;&lt;div&gt;&lt;/div&gt;&lt;/div&gt;&lt;/figure&gt;&lt;div&gt;&lt;/div&gt;&lt;/div&gt;&lt;span&gt;展开&lt;/span&gt;&lt;span&gt;收起&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;p&gt;重启mysql&lt;code&gt;sudo systemctl restart mysqld&lt;/code&gt;&lt;/p&gt;&lt;p&gt;重新登陆mysql,为slave机器授予replication权限&lt;/p&gt;&lt;div&gt;&lt;figure&gt;&lt;figcaption&gt;&lt;/figcaption&gt;&lt;pre&gt;&lt;code&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;1&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;grant&lt;/span&gt;&lt;span&gt; replication slave &lt;/span&gt;&lt;span&gt;on&lt;/span&gt;&lt;span&gt; *.* &lt;/span&gt;&lt;span&gt;to&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;&apos;repl_user&apos;&lt;/span&gt;&lt;span&gt;@&lt;/span&gt;&lt;span&gt;&apos;192.168.10.12&apos;&lt;/span&gt;&lt;span&gt; identified &lt;/span&gt;&lt;span&gt;by&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;&apos;&amp;lt;your-password&amp;gt;&apos;&lt;/span&gt;&lt;span&gt;;&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;/code&gt;&lt;/pre&gt;&lt;div&gt;&lt;div&gt;&lt;/div&gt;&lt;div&gt;&lt;/div&gt;&lt;/div&gt;&lt;/figure&gt;&lt;/div&gt;&lt;p&gt;查看master当前binlog状态信息&lt;/p&gt;&lt;div&gt;&lt;figure&gt;&lt;figcaption&gt;&lt;/figcaption&gt;&lt;pre&gt;&lt;code&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;1&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;show &lt;/span&gt;&lt;span&gt;master&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;status&lt;/span&gt;&lt;span&gt;;&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;/code&gt;&lt;/pre&gt;&lt;div&gt;&lt;div&gt;&lt;/div&gt;&lt;div&gt;&lt;/div&gt;&lt;/div&gt;&lt;/figure&gt;&lt;/div&gt;&lt;p&gt;&lt;/p&gt;&lt;figure&gt;&lt;img alt=&quot;20180319152143914570938.png&quot; loading=&quot;lazy&quot; width=&quot;1210&quot; height=&quot;232&quot; src=&quot;/_astro/20180319152143914570938.B2qmn2d-_Hdw4W.webp&quot; /&gt;&lt;figcaption&gt;20180319152143914570938.png&lt;/figcaption&gt;&lt;/figure&gt;&lt;p&gt;&lt;/p&gt;&lt;p&gt;在slaver机器上指定数据库master为192.168.10.11，并指定日志文件名字和位置，这样slaver就知道从哪里开始进行同步了。&lt;/p&gt;&lt;div&gt;&lt;figure&gt;&lt;figcaption&gt;&lt;/figcaption&gt;&lt;pre&gt;&lt;code&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;1&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;change &lt;/span&gt;&lt;span&gt;master&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;to&lt;/span&gt;&lt;span&gt;&lt;span&gt; master_host&lt;/span&gt;&lt;span&gt;=&lt;/span&gt;&lt;/span&gt;&lt;span&gt;&apos;192.168.10.11&apos;&lt;/span&gt;&lt;span&gt;&lt;span&gt;,master_user&lt;/span&gt;&lt;span&gt;=&lt;/span&gt;&lt;/span&gt;&lt;span&gt;&apos;repl_user&apos;&lt;/span&gt;&lt;span&gt;&lt;span&gt;,master_password&lt;/span&gt;&lt;span&gt;=&lt;/span&gt;&lt;/span&gt;&lt;span&gt;&apos;&amp;lt;your-password&amp;gt;&apos;&lt;/span&gt;&lt;span&gt;&lt;span&gt;,master_log_file&lt;/span&gt;&lt;span&gt;=&lt;/span&gt;&lt;/span&gt;&lt;span&gt;&apos;mysql-bin.000001&apos;&lt;/span&gt;&lt;span&gt;&lt;span&gt;,  master_log_pos&lt;/span&gt;&lt;span&gt;=&lt;/span&gt;&lt;/span&gt;&lt;span&gt;154&lt;/span&gt;&lt;span&gt;;&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;/code&gt;&lt;/pre&gt;&lt;div&gt;&lt;div&gt;&lt;/div&gt;&lt;div&gt;&lt;/div&gt;&lt;/div&gt;&lt;/figure&gt;&lt;/div&gt;&lt;p&gt;开启slaver&lt;/p&gt;&lt;div&gt;&lt;figure&gt;&lt;figcaption&gt;&lt;/figcaption&gt;&lt;pre&gt;&lt;code&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;1&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;start&lt;/span&gt;&lt;span&gt; slave&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;/code&gt;&lt;/pre&gt;&lt;div&gt;&lt;div&gt;&lt;/div&gt;&lt;div&gt;&lt;/div&gt;&lt;/div&gt;&lt;/figure&gt;&lt;/div&gt;&lt;p&gt;接下来在slave机器上进行同样的操作，给maser授予复制权限，然后查看slave机器的binlog状态信息（每个机器的不一定一样，这里和maser的状态一样）&lt;/p&gt;&lt;div&gt;&lt;figure&gt;&lt;figcaption&gt;&lt;/figcaption&gt;&lt;pre&gt;&lt;code&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;1&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;grant&lt;/span&gt;&lt;span&gt; replication slave &lt;/span&gt;&lt;span&gt;on&lt;/span&gt;&lt;span&gt; *.* &lt;/span&gt;&lt;span&gt;to&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;&apos;repl_user&apos;&lt;/span&gt;&lt;span&gt;@&lt;/span&gt;&lt;span&gt;&apos;192.168.10.11&apos;&lt;/span&gt;&lt;span&gt; identified &lt;/span&gt;&lt;span&gt;by&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;&apos;&amp;lt;your-password&amp;gt;&apos;&lt;/span&gt;&lt;span&gt;;&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;/code&gt;&lt;/pre&gt;&lt;div&gt;&lt;div&gt;&lt;/div&gt;&lt;div&gt;&lt;/div&gt;&lt;/div&gt;&lt;/figure&gt;&lt;/div&gt;&lt;p&gt;&lt;/p&gt;&lt;figure&gt;&lt;img alt=&quot;20180319152143914570938.png&quot; loading=&quot;lazy&quot; width=&quot;1210&quot; height=&quot;232&quot; src=&quot;/_astro/20180319152143914570938.B2qmn2d-_Hdw4W.webp&quot; /&gt;&lt;figcaption&gt;20180319152143914570938.png&lt;/figcaption&gt;&lt;/figure&gt;&lt;p&gt;&lt;/p&gt;&lt;p&gt;在master机器上指定数据库master为192.168.10.12,并指定日志文件名字和位置&lt;/p&gt;&lt;div&gt;&lt;figure&gt;&lt;figcaption&gt;&lt;/figcaption&gt;&lt;pre&gt;&lt;code&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;1&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;change &lt;/span&gt;&lt;span&gt;master&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;to&lt;/span&gt;&lt;span&gt;&lt;span&gt; master_host&lt;/span&gt;&lt;span&gt;=&lt;/span&gt;&lt;/span&gt;&lt;span&gt;&apos;192.168.10.12&apos;&lt;/span&gt;&lt;span&gt;&lt;span&gt;,master_user&lt;/span&gt;&lt;span&gt;=&lt;/span&gt;&lt;/span&gt;&lt;span&gt;&apos;repl_user&apos;&lt;/span&gt;&lt;span&gt;&lt;span&gt;,master_password&lt;/span&gt;&lt;span&gt;=&lt;/span&gt;&lt;/span&gt;&lt;span&gt;&apos;&amp;lt;your-password&amp;gt;&apos;&lt;/span&gt;&lt;span&gt;&lt;span&gt;,master_log_file&lt;/span&gt;&lt;span&gt;=&lt;/span&gt;&lt;/span&gt;&lt;span&gt;&apos;mysql-bin.000001&apos;&lt;/span&gt;&lt;span&gt;&lt;span&gt;,  master_log_pos&lt;/span&gt;&lt;span&gt;=&lt;/span&gt;&lt;/span&gt;&lt;span&gt;154&lt;/span&gt;&lt;span&gt;;&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;/code&gt;&lt;/pre&gt;&lt;div&gt;&lt;div&gt;&lt;/div&gt;&lt;div&gt;&lt;/div&gt;&lt;/div&gt;&lt;/figure&gt;&lt;/div&gt;&lt;p&gt;开启slaver&lt;/p&gt;&lt;div&gt;&lt;figure&gt;&lt;figcaption&gt;&lt;/figcaption&gt;&lt;pre&gt;&lt;code&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;1&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;start&lt;/span&gt;&lt;span&gt; slave&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;/code&gt;&lt;/pre&gt;&lt;div&gt;&lt;div&gt;&lt;/div&gt;&lt;div&gt;&lt;/div&gt;&lt;/div&gt;&lt;/figure&gt;&lt;/div&gt;&lt;/section&gt;
&lt;section&gt;&lt;h3&gt;防火墙设置&lt;a href=&quot;#防火墙设置&quot;&gt;&lt;span&gt;#&lt;/span&gt;&lt;/a&gt;&lt;/h3&gt;&lt;p&gt;开启3306端口&lt;/p&gt;&lt;div&gt;&lt;figure&gt;&lt;figcaption&gt;&lt;span&gt;&lt;/span&gt;&lt;span&gt;Terminal window&lt;/span&gt;&lt;/figcaption&gt;&lt;pre&gt;&lt;code&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;1&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;sudo&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;firewall-cmd&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;--zone=public&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;--add-port=3306/tcp&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;--permanent&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;2&lt;/div&gt;&lt;/div&gt;&lt;div&gt;
&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;3&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;sudo&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;firewall-cmd&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;--reload&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;/code&gt;&lt;/pre&gt;&lt;div&gt;&lt;div&gt;&lt;/div&gt;&lt;div&gt;&lt;/div&gt;&lt;/div&gt;&lt;/figure&gt;&lt;/div&gt;&lt;/section&gt;
&lt;section&gt;&lt;h3&gt;验证数据库同步&lt;a href=&quot;#验证数据库同步&quot;&gt;&lt;span&gt;#&lt;/span&gt;&lt;/a&gt;&lt;/h3&gt;&lt;p&gt;在两台机器上分别查看slave状态&lt;/p&gt;&lt;div&gt;&lt;figure&gt;&lt;figcaption&gt;&lt;/figcaption&gt;&lt;pre&gt;&lt;code&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;1&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;show slave &lt;/span&gt;&lt;span&gt;status&lt;/span&gt;&lt;span&gt;\G;&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;/code&gt;&lt;/pre&gt;&lt;div&gt;&lt;div&gt;&lt;/div&gt;&lt;div&gt;&lt;/div&gt;&lt;/div&gt;&lt;/figure&gt;&lt;/div&gt;&lt;p&gt;&lt;/p&gt;&lt;figure&gt;&lt;img alt=&quot;20180322152171000488344.png&quot; loading=&quot;lazy&quot; width=&quot;926&quot; height=&quot;122&quot; src=&quot;/_astro/20180322152171000488344.BoWlXClP_TWcl2.webp&quot; /&gt;&lt;figcaption&gt;20180322152171000488344.png&lt;/figcaption&gt;&lt;/figure&gt;&lt;p&gt;&lt;/p&gt;&lt;p&gt;必须确保两个参数 Slave_IO_Running，Slave_SQL_Running为Yes&lt;/p&gt;&lt;p&gt;都为yes后创建一个表，测试同步数据&lt;/p&gt;&lt;div&gt;&lt;figure&gt;&lt;figcaption&gt;&lt;/figcaption&gt;&lt;pre&gt;&lt;code&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;1&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;DROP&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;TABLE&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;IF&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;EXISTS&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;`test`&lt;/span&gt;&lt;span&gt;;&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;2&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;CREATE&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;TABLE&lt;/span&gt;&lt;span&gt; `&lt;/span&gt;&lt;span&gt;test&lt;/span&gt;&lt;span&gt;` (&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;3&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;  &lt;/span&gt;&lt;span&gt;`id`&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;int&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;11&lt;/span&gt;&lt;span&gt;) &lt;/span&gt;&lt;span&gt;NOT NULL&lt;/span&gt;&lt;span&gt; AUTO_INCREMENT,&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;4&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;  &lt;/span&gt;&lt;span&gt;PRIMARY KEY&lt;/span&gt;&lt;span&gt; (&lt;/span&gt;&lt;span&gt;`id`&lt;/span&gt;&lt;span&gt;)&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;5&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;&lt;span&gt;) ENGINE&lt;/span&gt;&lt;span&gt;=&lt;/span&gt;&lt;span&gt;InnoDB &lt;/span&gt;&lt;/span&gt;&lt;span&gt;DEFAULT&lt;/span&gt;&lt;span&gt;&lt;span&gt; CHARSET&lt;/span&gt;&lt;span&gt;=&lt;/span&gt;&lt;span&gt;utf8;&lt;/span&gt;&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;6&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;insert&lt;/span&gt;&lt;span&gt; test &lt;/span&gt;&lt;span&gt;values&lt;/span&gt;&lt;span&gt;();&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;7&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;select&lt;/span&gt;&lt;span&gt; * &lt;/span&gt;&lt;span&gt;from&lt;/span&gt;&lt;span&gt; test;&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;/code&gt;&lt;/pre&gt;&lt;div&gt;&lt;div&gt;&lt;/div&gt;&lt;div&gt;&lt;/div&gt;&lt;/div&gt;&lt;/figure&gt;&lt;/div&gt;&lt;p&gt;在master机器上执行上面的操作,返回结构，&lt;/p&gt;&lt;p&gt;&lt;/p&gt;&lt;figure&gt;&lt;img alt=&quot;20180322152171091923913.png&quot; loading=&quot;lazy&quot; width=&quot;146&quot; height=&quot;168&quot; src=&quot;/_astro/20180322152171091923913.x3oCZPaR_ZmXBXO.webp&quot; /&gt;&lt;figcaption&gt;20180322152171091923913.png&lt;/figcaption&gt;&lt;/figure&gt;&lt;p&gt;&lt;/p&gt;&lt;p&gt;在slave上也执行查询语句&lt;code&gt;select * from test;&lt;/code&gt;,返回同样的结果。再在master上执行&lt;code&gt;insert test values();&lt;/code&gt;返回结果如下&lt;/p&gt;&lt;p&gt;&lt;/p&gt;&lt;figure&gt;&lt;img alt=&quot;20180322152171094816591.png&quot; loading=&quot;lazy&quot; width=&quot;146&quot; height=&quot;200&quot; src=&quot;/_astro/20180322152171094816591.Bd7dSCz0_1UN0eq.webp&quot; /&gt;&lt;figcaption&gt;20180322152171094816591.png&lt;/figcaption&gt;&lt;/figure&gt;&lt;p&gt;&lt;/p&gt;&lt;p&gt;可以看到master上的所有自增主键都是奇数，接着在slave机器上执行&lt;code&gt;insert test values();&lt;/code&gt;语句，结果为123，slave为偶数，从2开始，说明我们的自增配置已经生效，并且也进行了数据同步；&lt;/p&gt;&lt;/section&gt;
&lt;section&gt;&lt;h3&gt;下载安装keepalived&lt;a href=&quot;#下载安装keepalived&quot;&gt;&lt;span&gt;#&lt;/span&gt;&lt;/a&gt;&lt;/h3&gt;&lt;p&gt;&lt;code&gt;yum install keepalived&lt;/code&gt;下载keepalived，keepalived只有一个配置文件，默认位于/etc/keepalived/keepalived.conf&lt;/p&gt;&lt;p&gt;在配置之前先查看本地机器的网卡设置，使用&lt;code&gt;ip a&lt;/code&gt;命令,得知当前ip使用的是网卡&lt;strong&gt;eth0&lt;/strong&gt;,这个要根据&lt;code&gt;ip a&lt;/code&gt;命令执行结果得到。&lt;strong&gt;网卡名称将在接下来的keepalived配置文件和防火墙设置中用到&lt;/strong&gt;。&lt;/p&gt;&lt;div&gt;&lt;figure&gt;&lt;figcaption&gt;&lt;span&gt;&lt;/span&gt;&lt;span&gt;Terminal window&lt;/span&gt;&lt;/figcaption&gt;&lt;pre&gt;&lt;code&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;1&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;vim&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;/etc/keepalived/keepalived.conf&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;/code&gt;&lt;/pre&gt;&lt;div&gt;&lt;div&gt;&lt;/div&gt;&lt;div&gt;&lt;/div&gt;&lt;/div&gt;&lt;/figure&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;&lt;figure&gt;&lt;figcaption&gt;&lt;/figcaption&gt;&lt;pre&gt;&lt;code&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;1&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;! &lt;/span&gt;&lt;span&gt;Configuration&lt;/span&gt;&lt;span&gt; File for keepalived&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;2&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;global_defs&lt;/span&gt;&lt;span&gt; {&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;3&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;   &lt;/span&gt;&lt;span&gt;router_id&lt;/span&gt;&lt;span&gt; mysql-1&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;4&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;}&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;5&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;#group&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;6&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;vrrp_sync_group&lt;/span&gt;&lt;span&gt; VG1 {&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;7&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;      &lt;/span&gt;&lt;span&gt;group&lt;/span&gt;&lt;span&gt; {&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;8&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;              &lt;/span&gt;&lt;span&gt;HA_1&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;9&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;&lt;span&gt;               &lt;/span&gt;&lt;/span&gt;&lt;span&gt;}&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;10&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;&lt;span&gt;            &lt;/span&gt;&lt;/span&gt;&lt;span&gt;}&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;11&lt;/div&gt;&lt;/div&gt;&lt;div&gt;
&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;12&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;vrrp_script&lt;/span&gt;&lt;span&gt; check_mysql {&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;13&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;script&lt;/span&gt;&lt;span&gt; /etc/keepalived/bin/check.sh&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;14&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;interval&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;2&lt;/span&gt;&lt;span&gt;   &lt;/span&gt;&lt;span&gt;#执行间隔&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;15&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;weight&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;20&lt;/span&gt;&lt;span&gt;    &lt;/span&gt;&lt;span&gt;#权重&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;16&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;}&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;17&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;vrrp_instance&lt;/span&gt;&lt;span&gt; HA_1 {&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;18&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;   &lt;span&gt; &lt;/span&gt;&lt;/span&gt;&lt;span&gt;state &lt;/span&gt;&lt;span&gt;BACKUP  &lt;/span&gt;&lt;span&gt;#两个节点最好都设置成BACKUP模式，避免因为意外情况下（比如脑裂）相互抢占导致往两个节点写入相同数据而引发冲突；&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;19&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;&lt;span&gt;    &lt;/span&gt;&lt;/span&gt;&lt;span&gt;interface eth0  &lt;/span&gt;&lt;span&gt;#网卡&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;20&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;&lt;span&gt;    &lt;/span&gt;&lt;/span&gt;&lt;span&gt;virtual_router_id &lt;/span&gt;&lt;span&gt;51&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;#两台机器要一致&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;21&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;&lt;span&gt;    &lt;/span&gt;&lt;/span&gt;&lt;span&gt;priority &lt;/span&gt;&lt;span&gt;100&lt;/span&gt;&lt;span&gt;         &lt;/span&gt;&lt;span&gt;#初始权重，master机器配置为100，slave配置为90&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;22&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;&lt;span&gt;    &lt;/span&gt;&lt;/span&gt;&lt;span&gt;nopreempt            &lt;/span&gt;&lt;span&gt;#不抢占，当slave机器正常工作时，master恢复正常后不抢占slave的vip&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;23&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;&lt;span&gt;    &lt;/span&gt;&lt;/span&gt;&lt;span&gt;advert_int &lt;/span&gt;&lt;span&gt;1&lt;/span&gt;&lt;span&gt;         &lt;/span&gt;&lt;span&gt;#检查间隔，两台机器一致&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;24&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;&lt;span&gt;    &lt;/span&gt;&lt;/span&gt;&lt;span&gt;authentication {&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;25&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;&lt;span&gt;        &lt;/span&gt;&lt;/span&gt;&lt;span&gt;auth_type PASS&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;26&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;&lt;span&gt;        &lt;/span&gt;&lt;/span&gt;&lt;span&gt;auth_pass &lt;/span&gt;&lt;span&gt;1111&lt;/span&gt;&lt;span&gt;   &lt;/span&gt;&lt;span&gt;#两台一致&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;27&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;&lt;span&gt;    &lt;/span&gt;&lt;/span&gt;&lt;span&gt;}&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;28&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;&lt;span&gt;    &lt;/span&gt;&lt;/span&gt;&lt;span&gt;virtual_ipaddress {&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;29&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;      &lt;/span&gt;&lt;span&gt;192.168.10.100&lt;/span&gt;&lt;span&gt;  &lt;/span&gt;&lt;span&gt;#vip&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;30&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;&lt;span&gt;    &lt;/span&gt;&lt;/span&gt;&lt;span&gt;}&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;31&lt;/div&gt;&lt;/div&gt;&lt;div&gt;
&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;32&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;&lt;span&gt;    &lt;/span&gt;&lt;/span&gt;&lt;span&gt;track_script {&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;33&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;&lt;span&gt;        &lt;/span&gt;&lt;/span&gt;&lt;span&gt;check_mysql&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;34&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;&lt;span&gt;    &lt;/span&gt;&lt;/span&gt;&lt;span&gt;}&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;35&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;}&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;/code&gt;&lt;/pre&gt;&lt;div&gt;&lt;div&gt;&lt;/div&gt;&lt;div&gt;&lt;/div&gt;&lt;/div&gt;&lt;/figure&gt;&lt;div&gt;&lt;/div&gt;&lt;/div&gt;&lt;span&gt;展开&lt;/span&gt;&lt;span&gt;收起&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;section&gt;&lt;h4&gt;keepalived的配置文件说明&lt;a href=&quot;#keepalived的配置文件说明&quot;&gt;&lt;span&gt;#&lt;/span&gt;&lt;/a&gt;&lt;/h4&gt;&lt;p&gt;vrrp_sync_group的应用场景为：如果路由有2个网段，一个内网，一个外网，每个网段开启一个VRRP实例，假设VRRP配置为检查内网，那么当外网出现问题 时，VRRPD会认为自己是健康的，则不会发送Master和Backup的切换，从而导致问题，Sync Group可以把两个实例都放入Sync Group，这样的话，Group 里任何一个实例出现问题都会发生切换。在这里其实并没有用到group，因为我们只设置了一个实例。&lt;/p&gt;&lt;p&gt;vrrp_instance配置了检测脚本，vip等信息。两台机器的配置基本一致，除了priority参数（master设置为100，slave设置为90）。当master机器keepalivde启动后，将会去执行check.sh，每两秒执行一次，如果执行结果返回零，将在初始权重上加上20，maste的权重将达到120，同理，slave的权重为110（90+20）。这时，vip将漂移到master机器上。如果master上的check.sh执行结果返回1,那么master的权重将不会加20，那么master的权重为100，小于110，此时vip将漂移到slave机器上。keepalived就是这样根据权重的大小进行vip漂移的。另外，需要注意的是，&lt;strong&gt;检测脚本的权重一定要大于两台机器的初始权重&lt;/strong&gt;，20&amp;gt;(100-90),因为只有这样，检测脚本检测到服务异常时才能影响到权重的总和。&lt;/p&gt;&lt;p&gt;vrrp_script配置了检测脚本和被检测的服务的权重，以及检测频率等。这里的关键是check.sh的实现。这个脚本这里主要是用来检测mysql是否正常服务，而正常服务的检测可以很简单，也可以很复杂，比如简单的检测mysqld服务是否正常启动，复杂的可以用mysql命令登陆数据库，执行sql语句，查看mysql的状态，进行判断。这里我们只是简单的检测了mysqld服务是否正常启动；&lt;/p&gt;&lt;div&gt;&lt;figure&gt;&lt;figcaption&gt;&lt;/figcaption&gt;&lt;pre&gt;&lt;code&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;1&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;#!/bin/bash&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;2&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;killall&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;-0&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;mysqld&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;zabbix_proxy&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;/code&gt;&lt;/pre&gt;&lt;div&gt;&lt;div&gt;&lt;/div&gt;&lt;div&gt;&lt;/div&gt;&lt;/div&gt;&lt;/figure&gt;&lt;/div&gt;&lt;p&gt;试了了killall命令，如果没有该命令需要安装&lt;code&gt;sudo yum install psmisc -y&lt;/code&gt;,&lt;code&gt;-0&lt;/code&gt; 是检测服务正常运行的信号量，如果正常运行返回0，不正常返回1，支持传入多个服务；&lt;/p&gt;&lt;p&gt;同时需要注意的是记得为该脚本授权&lt;code&gt;chmod +x /etc/keepalived/bin/check.sh&lt;/code&gt;;&lt;/p&gt;&lt;p&gt;&lt;code&gt;sudo vim check.sh&lt;/code&gt;&lt;/p&gt;&lt;div&gt;&lt;figure&gt;&lt;figcaption&gt;&lt;/figcaption&gt;&lt;pre&gt;&lt;code&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;1&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;#!/bin/bash&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;2&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;sudo&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;killall&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;-0&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;mysqld&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;zabbix-proxy&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;/code&gt;&lt;/pre&gt;&lt;div&gt;&lt;div&gt;&lt;/div&gt;&lt;div&gt;&lt;/div&gt;&lt;/div&gt;&lt;/figure&gt;&lt;/div&gt;&lt;/section&gt;&lt;section&gt;&lt;h4&gt;Keepalived防火墙设置&lt;a href=&quot;#keepalived防火墙设置&quot;&gt;&lt;span&gt;#&lt;/span&gt;&lt;/a&gt;&lt;/h4&gt;&lt;p&gt;keepalived需要执行下面两条语句，对防火墙进行操作，才能确保两台keepalived能够进行选举,如果没有进行防火墙设置，则会出现两台机器都绑定了vip（脑裂）,原因就是两台机器没法进行通信，无法找到真正的master;&lt;/p&gt;&lt;div&gt;&lt;figure&gt;&lt;figcaption&gt;&lt;span&gt;&lt;/span&gt;&lt;span&gt;Terminal window&lt;/span&gt;&lt;/figcaption&gt;&lt;pre&gt;&lt;code&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;1&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;sudo&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;firewall-cmd&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;--direct&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;--permanent&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;--add-rule&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;ipv4&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;filter&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;INPUT&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;0&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;--in-interface&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;eth0&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;--destination&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;224.0.0.18&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;--protocol&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;vrrp&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;-j&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;ACCEPT&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;2&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;sudo&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;firewall-cmd&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;--direct&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;--permanent&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;--add-rule&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;ipv4&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;filter&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;OUT&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;0&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;--out-interface&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;eth0&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;--destination&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;224.0.0.18&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;--protocol&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;vrrp&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;-j&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;ACCEPT&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;3&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;sudo&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;firewall-cmd&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;--reload&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;/code&gt;&lt;/pre&gt;&lt;div&gt;&lt;div&gt;&lt;/div&gt;&lt;div&gt;&lt;/div&gt;&lt;/div&gt;&lt;/figure&gt;&lt;/div&gt;&lt;/section&gt;&lt;/section&gt;
&lt;section&gt;&lt;h3&gt;启动 zabbix-proxy&lt;a href=&quot;#启动-zabbix-proxy&quot;&gt;&lt;span&gt;#&lt;/span&gt;&lt;/a&gt;&lt;/h3&gt;&lt;section&gt;&lt;h4&gt;修改配置文件&lt;a href=&quot;#修改配置文件&quot;&gt;&lt;span&gt;#&lt;/span&gt;&lt;/a&gt;&lt;/h4&gt;&lt;p&gt;&lt;code&gt;vim /etc/zabbix/zabbix_proxy.conf&lt;/code&gt;&lt;/p&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;&lt;figure&gt;&lt;figcaption&gt;&lt;/figcaption&gt;&lt;pre&gt;&lt;code&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;1&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;ProxyMode&lt;/span&gt;&lt;span&gt;&lt;span&gt;=&lt;/span&gt;&lt;span&gt;1&lt;/span&gt;&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;2&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;Server&lt;/span&gt;&lt;span&gt;&lt;span&gt;=&lt;/span&gt;&lt;span&gt;&amp;lt;zabbix-server-ip&amp;gt;&lt;/span&gt;&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;3&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;Hostname&lt;/span&gt;&lt;span&gt;&lt;span&gt;=&lt;/span&gt;&lt;span&gt;192.168.10.100  &lt;/span&gt;&lt;/span&gt;&lt;span&gt;#vip&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;4&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;LogFile&lt;/span&gt;&lt;span&gt;&lt;span&gt;=&lt;/span&gt;&lt;span&gt;/var/log/zabbix/zabbix_proxy.log&lt;/span&gt;&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;5&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;LogFileSize&lt;/span&gt;&lt;span&gt;&lt;span&gt;=&lt;/span&gt;&lt;span&gt;0&lt;/span&gt;&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;6&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;PidFile&lt;/span&gt;&lt;span&gt;&lt;span&gt;=&lt;/span&gt;&lt;span&gt;/var/run/zabbix/zabbix_proxy.pid&lt;/span&gt;&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;7&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;SocketDir&lt;/span&gt;&lt;span&gt;&lt;span&gt;=&lt;/span&gt;&lt;span&gt;/var/run/zabbix&lt;/span&gt;&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;8&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;DBHost&lt;/span&gt;&lt;span&gt;&lt;span&gt;=&lt;/span&gt;&lt;span&gt;localhost&lt;/span&gt;&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;9&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;DBName&lt;/span&gt;&lt;span&gt;&lt;span&gt;=&lt;/span&gt;&lt;span&gt;zabbix_proxy&lt;/span&gt;&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;10&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;DBUser&lt;/span&gt;&lt;span&gt;&lt;span&gt;=&lt;/span&gt;&lt;span&gt;zabbix&lt;/span&gt;&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;11&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;DBPassword&lt;/span&gt;&lt;span&gt;&lt;span&gt;=&lt;/span&gt;&lt;span&gt;&amp;lt;zabbix-db-password&amp;gt;&lt;/span&gt;&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;12&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;HeartbeatFrequency&lt;/span&gt;&lt;span&gt;&lt;span&gt;=&lt;/span&gt;&lt;span&gt;10&lt;/span&gt;&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;13&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;ConfigFrequency&lt;/span&gt;&lt;span&gt;&lt;span&gt;=&lt;/span&gt;&lt;span&gt;60&lt;/span&gt;&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;14&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;DataSenderFrequency&lt;/span&gt;&lt;span&gt;&lt;span&gt;=&lt;/span&gt;&lt;span&gt;5&lt;/span&gt;&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;15&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;StartPollers&lt;/span&gt;&lt;span&gt;&lt;span&gt;=&lt;/span&gt;&lt;span&gt;50&lt;/span&gt;&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;16&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;StartPollersUnreachable&lt;/span&gt;&lt;span&gt;&lt;span&gt;=&lt;/span&gt;&lt;span&gt;10&lt;/span&gt;&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;17&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;StartTrappers&lt;/span&gt;&lt;span&gt;&lt;span&gt;=&lt;/span&gt;&lt;span&gt;10&lt;/span&gt;&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;18&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;StartPingers&lt;/span&gt;&lt;span&gt;&lt;span&gt;=&lt;/span&gt;&lt;span&gt;10&lt;/span&gt;&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;19&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;SNMPTrapperFile&lt;/span&gt;&lt;span&gt;&lt;span&gt;=&lt;/span&gt;&lt;span&gt;/var/log/snmptrap/snmptrap.log&lt;/span&gt;&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;20&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;CacheSize&lt;/span&gt;&lt;span&gt;&lt;span&gt;=&lt;/span&gt;&lt;span&gt;8G&lt;/span&gt;&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;21&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;StartDBSyncers&lt;/span&gt;&lt;span&gt;&lt;span&gt;=&lt;/span&gt;&lt;span&gt;10&lt;/span&gt;&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;22&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;HistoryCacheSize&lt;/span&gt;&lt;span&gt;&lt;span&gt;=&lt;/span&gt;&lt;span&gt;2G&lt;/span&gt;&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;23&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;HistoryIndexCacheSize&lt;/span&gt;&lt;span&gt;&lt;span&gt;=&lt;/span&gt;&lt;span&gt;2G&lt;/span&gt;&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;24&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;Timeout&lt;/span&gt;&lt;span&gt;&lt;span&gt;=&lt;/span&gt;&lt;span&gt;30&lt;/span&gt;&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;25&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;#DebugLevel=4&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;26&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;#ExternalScripts=/usr/lib/zabbix/externalscripts&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;27&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;#LogSlowQueries=3000&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;/code&gt;&lt;/pre&gt;&lt;div&gt;&lt;div&gt;&lt;/div&gt;&lt;div&gt;&lt;/div&gt;&lt;/div&gt;&lt;/figure&gt;&lt;div&gt;&lt;/div&gt;&lt;/div&gt;&lt;span&gt;展开&lt;/span&gt;&lt;span&gt;收起&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;figure&gt;&lt;figcaption&gt;&lt;span&gt;&lt;/span&gt;&lt;span&gt;Terminal window&lt;/span&gt;&lt;/figcaption&gt;&lt;pre&gt;&lt;code&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;1&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;#开启端口&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;2&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;sudo&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;firewall-cmd&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;--zone=public&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;--add-port=10051/tcp&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;--permanent&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;3&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;sudo&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;firewall-cmd&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;--reload&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;4&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;#启动zabbix-proxy&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;5&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;systemctl&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;start&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;zabbix-proxy&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;/code&gt;&lt;/pre&gt;&lt;div&gt;&lt;div&gt;&lt;/div&gt;&lt;div&gt;&lt;/div&gt;&lt;/div&gt;&lt;/figure&gt;&lt;/div&gt;&lt;/section&gt;&lt;section&gt;&lt;h4&gt;开机自动启动&lt;a href=&quot;#开机自动启动&quot;&gt;&lt;span&gt;#&lt;/span&gt;&lt;/a&gt;&lt;/h4&gt;&lt;div&gt;&lt;figure&gt;&lt;figcaption&gt;&lt;span&gt;&lt;/span&gt;&lt;span&gt;Terminal window&lt;/span&gt;&lt;/figcaption&gt;&lt;pre&gt;&lt;code&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;1&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;systemctl&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;enable&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;zabbix-proxy&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;2&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;systemctl&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;enable&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;mysqld&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;/code&gt;&lt;/pre&gt;&lt;div&gt;&lt;div&gt;&lt;/div&gt;&lt;div&gt;&lt;/div&gt;&lt;/div&gt;&lt;/figure&gt;&lt;/div&gt;&lt;/section&gt;&lt;/section&gt;
&lt;section&gt;&lt;h3&gt;测试方法&lt;a href=&quot;#测试方法&quot;&gt;&lt;span&gt;#&lt;/span&gt;&lt;/a&gt;&lt;/h3&gt;&lt;p&gt;1.查看vip绑定情况 ip a&lt;/p&gt;&lt;p&gt;2.通过连接vip的mysql，&lt;/p&gt;&lt;div&gt;&lt;figure&gt;&lt;figcaption&gt;&lt;span&gt;&lt;/span&gt;&lt;span&gt;Terminal window&lt;/span&gt;&lt;/figcaption&gt;&lt;pre&gt;&lt;code&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;1&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;mysql&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;-h&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;192.168.10.100&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;-uroot&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;-p&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;/code&gt;&lt;/pre&gt;&lt;div&gt;&lt;div&gt;&lt;/div&gt;&lt;div&gt;&lt;/div&gt;&lt;/div&gt;&lt;/figure&gt;&lt;/div&gt;&lt;div&gt;&lt;figure&gt;&lt;figcaption&gt;&lt;/figcaption&gt;&lt;pre&gt;&lt;code&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;1&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;show variables &lt;/span&gt;&lt;span&gt;like&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;&quot;%hostname%&quot;&lt;/span&gt;&lt;span&gt;;&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;/code&gt;&lt;/pre&gt;&lt;div&gt;&lt;div&gt;&lt;/div&gt;&lt;div&gt;&lt;/div&gt;&lt;/div&gt;&lt;/figure&gt;&lt;/div&gt;&lt;p&gt;查看返回的hostaname,就知道了当前连接是哪台机器&lt;/p&gt;&lt;/section&gt;
&lt;section&gt;&lt;h3&gt;故障切换演练&lt;a href=&quot;#故障切换演练&quot;&gt;&lt;span&gt;#&lt;/span&gt;&lt;/a&gt;&lt;/h3&gt;&lt;p&gt;搭完一定要演练，不然真出故障时才发现切不过去。演练按这个顺序做：&lt;/p&gt;&lt;ol&gt;
&lt;li&gt;&lt;strong&gt;确认初始状态&lt;/strong&gt;：在两台机器上执行 &lt;code&gt;ip a&lt;/code&gt;，VIP 应该只绑在 master 上；通过 VIP 连 MySQL，执行 &lt;code&gt;show variables like &apos;hostname&apos;&lt;/code&gt;，确认连的是 master。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;停 master 的 MySQL&lt;/strong&gt;：&lt;code&gt;systemctl stop mysqld&lt;/code&gt;。检测脚本返回非 0，master 权重降到 100，低于 slave 的 110，VIP 应在几秒内漂到 slave。再通过 VIP 查一次 hostname 确认。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;验证数据不丢&lt;/strong&gt;：切换前后各写几条测试数据，恢复 master 后检查两边的 &lt;code&gt;show slave status&lt;/code&gt;，两个线程都是 Yes，数据一致。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;验证不抢占&lt;/strong&gt;：恢复 master 后，因为配置了 &lt;code&gt;nopreempt&lt;/code&gt;，VIP 应该留在 slave 上，不会来回漂。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;模拟网络隔离&lt;/strong&gt;：用防火墙临时屏蔽 VRRP 报文，观察是否出现两台机器同时持有 VIP（脑裂），确认告警能发出来。&lt;/li&gt;
&lt;/ol&gt;&lt;/section&gt;
&lt;section&gt;&lt;h3&gt;双主模式容易踩的坑&lt;a href=&quot;#双主模式容易踩的坑&quot;&gt;&lt;span&gt;#&lt;/span&gt;&lt;/a&gt;&lt;/h3&gt;&lt;p&gt;&lt;strong&gt;1. 两边同时写，主键冲突。&lt;/strong&gt; 用 &lt;code&gt;auto-increment-increment = 2&lt;/code&gt; 和不同的 &lt;code&gt;auto-increment-offset&lt;/code&gt;，让一台只生成奇数主键、另一台只生成偶数主键。但这只解决自增主键，唯一索引冲突、同一行被两边同时修改的问题依然存在。所以双主虽然两边都能写，&lt;strong&gt;实际使用时最好只往 VIP 所在的那台写&lt;/strong&gt;，另一台只做热备。&lt;/p&gt;&lt;p&gt;&lt;strong&gt;2. &lt;code&gt;slave_skip_errors&lt;/code&gt; 会掩盖数据不一致。&lt;/strong&gt; 文中为了让同步不中断，跳过了 1062（主键重复）、1032（记录不存在）等错误。这在监控数据这种可以容忍少量丢失的场景能用，但它的代价是两边数据悄悄地不一致，&lt;strong&gt;业务数据库千万不要这样配&lt;/strong&gt;。&lt;/p&gt;&lt;p&gt;&lt;strong&gt;3. 检测脚本太简单。&lt;/strong&gt; &lt;code&gt;killall -0&lt;/code&gt; 只能判断进程还在，进程活着但 MySQL 卡死、复制线程已经停了，它都发现不了。更稳妥的检测是真正登录执行一条查询，并检查 &lt;code&gt;Slave_IO_Running&lt;/code&gt; 和 &lt;code&gt;Slave_SQL_Running&lt;/code&gt;。&lt;/p&gt;&lt;p&gt;&lt;strong&gt;4. 脑裂。&lt;/strong&gt; 两台机器之间 VRRP 报文不通时，都会认为对方挂了，各自绑上 VIP。防火墙放行 VRRP（协议号 112，组播地址 224.0.0.18）是最基本的；两边都设为 &lt;code&gt;BACKUP&lt;/code&gt; 加 &lt;code&gt;nopreempt&lt;/code&gt; 可以减少来回抢占；条件允许的话再加一条独立的心跳链路。&lt;/p&gt;&lt;p&gt;&lt;strong&gt;5. 复制延迟。&lt;/strong&gt; 主库写入压力大、大事务、单线程回放都会导致从库跟不上。切换发生时，延迟期间的数据就可能丢失。MySQL 5.7 起可以开启基于组提交的并行复制（&lt;code&gt;slave_parallel_type = LOGICAL_CLOCK&lt;/code&gt;）缓解。&lt;/p&gt;&lt;/section&gt;
&lt;section&gt;&lt;h3&gt;现在回头看&lt;a href=&quot;#现在回头看&quot;&gt;&lt;span&gt;#&lt;/span&gt;&lt;/a&gt;&lt;/h3&gt;&lt;p&gt;这套方案在 2018 年是很典型的做法，简单、好理解。放到现在，我会做几处改进：&lt;/p&gt;&lt;ul&gt;
&lt;li&gt;用 &lt;strong&gt;GTID&lt;/strong&gt; 代替手动指定 binlog 文件名和位置，切换和重建复制都简单很多；&lt;/li&gt;
&lt;li&gt;更严格的一致性需求，考虑 &lt;strong&gt;MySQL Group Replication&lt;/strong&gt; 或者 Orchestrator 这类自动故障转移工具，而不是靠 Keepalived 判断数据库状态；&lt;/li&gt;
&lt;li&gt;如果在云上，直接用托管数据库的高可用版本，大多数云环境也不支持 VRRP 组播漂移 VIP。&lt;/li&gt;
&lt;/ul&gt;&lt;/section&gt;</content:encoded></item><item><title>Nginx 与 OpenResty：从编译调优到 CDN 边缘的 Lua 定制</title><link>https://tracerzzz.cn/posts/nginx-usage/</link><guid isPermaLink="true">https://tracerzzz.cn/posts/nginx-usage/</guid><description>我在电信、百度做 CDN 时和 Nginx 打交道最多。这篇从编译、常用命令、性能调优讲起，重点讲 OpenResty 的请求处理阶段，以及在 CDN 边缘用 Lua 实现防盗链、URI 改写和回源 AWS S3 V4 签名鉴权的思路。</description><pubDate>Mon, 15 Jan 2018 08:37:11 GMT</pubDate><content:encoded>&lt;blockquote&gt;&lt;p&gt;&lt;strong&gt;关于这篇文章&lt;/strong&gt;：最初写于 2018 年，当时只是一份 Nginx 编译和常用命令的记录。2026 年 10 月重写：第一到三节保留了当年的内容（压测数据也是 2018 年的），并修正了过时的配置；第四到六节是新增的，来自在中国电信、百度云 CDN 工作期间做 Nginx + Lua 定制开发的经验。&lt;/p&gt;&lt;/blockquote&gt;
&lt;section&gt;&lt;h2&gt;一、编译安装&lt;a href=&quot;#一编译安装&quot;&gt;&lt;span&gt;#&lt;/span&gt;&lt;/a&gt;&lt;/h2&gt;&lt;p&gt;需要第三方模块（比如 &lt;code&gt;nginx-rtmp-module&lt;/code&gt;）时，只能从源码编译。以 CentOS 7 为例：&lt;/p&gt;&lt;div&gt;&lt;figure&gt;&lt;figcaption&gt;&lt;span&gt;&lt;/span&gt;&lt;span&gt;Terminal window&lt;/span&gt;&lt;/figcaption&gt;&lt;pre&gt;&lt;code&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;1&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;# 依赖：正则（rewrite、location 正则匹配）、zlib（gzip）、openssl（https）&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;2&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;yum&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;install&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;-y&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;gcc&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;pcre-devel&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;zlib-devel&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;openssl-devel&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;3&lt;/div&gt;&lt;/div&gt;&lt;div&gt;
&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;4&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;wget&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;http://nginx.org/download/nginx-1.12.2.tar.gz&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;5&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;tar&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;zxvf&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;nginx-1.12.2.tar.gz&lt;/span&gt;&lt;span&gt; &amp;amp;&amp;amp; &lt;/span&gt;&lt;span&gt;cd&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;nginx-1.12.2&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;6&lt;/div&gt;&lt;/div&gt;&lt;div&gt;
&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;7&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;./configure&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;--prefix=/usr/local/nginx&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;\&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;8&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;  &lt;/span&gt;&lt;span&gt;--with-http_ssl_module&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;\&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;9&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;  &lt;/span&gt;&lt;span&gt;--with-debug&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;\&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;10&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;  &lt;/span&gt;&lt;span&gt;--add-module=../nginx-rtmp-module&lt;/span&gt;&lt;span&gt;   &lt;/span&gt;&lt;span&gt;# 按需添加第三方模块&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;11&lt;/div&gt;&lt;/div&gt;&lt;div&gt;
&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;12&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;make&lt;/span&gt;&lt;span&gt; &amp;amp;&amp;amp; &lt;/span&gt;&lt;span&gt;make&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;install&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;/code&gt;&lt;/pre&gt;&lt;div&gt;&lt;div&gt;&lt;/div&gt;&lt;div&gt;&lt;/div&gt;&lt;/div&gt;&lt;/figure&gt;&lt;/div&gt;&lt;p&gt;现在更推荐直接用 &lt;a href=&quot;https://openresty.org/&quot; target=&quot;_blank&quot;&gt;OpenResty&lt;/a&gt;：它是 Nginx 加上 LuaJIT 和一组常用 Lua 库的发行版，后面的 Lua 定制都基于它。Lua 第三方库可以用 OpenResty 自带的 &lt;code&gt;opm&lt;/code&gt; 安装，或者用 LuaRocks 指定 OpenResty 的 LuaJIT 编译安装：&lt;/p&gt;&lt;div&gt;&lt;figure&gt;&lt;figcaption&gt;&lt;span&gt;&lt;/span&gt;&lt;span&gt;Terminal window&lt;/span&gt;&lt;/figcaption&gt;&lt;pre&gt;&lt;code&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;1&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;./configure&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;--with-lua=/usr/local/openresty/luajit&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;\&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;2&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;  &lt;/span&gt;&lt;span&gt;--with-lua-include=/usr/local/openresty/luajit/include/luajit-2.1/&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;3&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;make&lt;/span&gt;&lt;span&gt; &amp;amp;&amp;amp; &lt;/span&gt;&lt;span&gt;make&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;install&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;/code&gt;&lt;/pre&gt;&lt;div&gt;&lt;div&gt;&lt;/div&gt;&lt;div&gt;&lt;/div&gt;&lt;/div&gt;&lt;/figure&gt;&lt;/div&gt;&lt;/section&gt;
&lt;section&gt;&lt;h2&gt;二、常用命令与 gzip&lt;a href=&quot;#二常用命令与-gzip&quot;&gt;&lt;span&gt;#&lt;/span&gt;&lt;/a&gt;&lt;/h2&gt;&lt;div&gt;&lt;figure&gt;&lt;figcaption&gt;&lt;span&gt;&lt;/span&gt;&lt;span&gt;Terminal window&lt;/span&gt;&lt;/figcaption&gt;&lt;pre&gt;&lt;code&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;1&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;nginx&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;-t&lt;/span&gt;&lt;span&gt;          &lt;/span&gt;&lt;span&gt;# 检查配置语法，改完配置先跑这个&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;2&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;nginx&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;-s&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;reload&lt;/span&gt;&lt;span&gt;   &lt;/span&gt;&lt;span&gt;# 平滑重载：新 worker 接新请求，老 worker 处理完再退出&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;3&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;nginx&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;-s&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;quit&lt;/span&gt;&lt;span&gt;     &lt;/span&gt;&lt;span&gt;# 优雅退出&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;4&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;nginx&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;-V&lt;/span&gt;&lt;span&gt;          &lt;/span&gt;&lt;span&gt;# 查看编译参数和已编译的模块&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;/code&gt;&lt;/pre&gt;&lt;div&gt;&lt;div&gt;&lt;/div&gt;&lt;div&gt;&lt;/div&gt;&lt;/div&gt;&lt;/figure&gt;&lt;/div&gt;&lt;p&gt;gzip 配置。当年的版本把 jpeg、png 也加进了 &lt;code&gt;gzip_types&lt;/code&gt;，这是个常见错误：图片本身已经压缩过，再 gzip 只会白白消耗 CPU。&lt;/p&gt;&lt;div&gt;&lt;figure&gt;&lt;figcaption&gt;&lt;/figcaption&gt;&lt;pre&gt;&lt;code&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;1&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;gzip &lt;/span&gt;&lt;span&gt;on&lt;/span&gt;&lt;span&gt;;&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;2&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;gzip_min_length &lt;/span&gt;&lt;span&gt;1k&lt;/span&gt;&lt;span&gt;;          &lt;/span&gt;&lt;span&gt;# 太小的响应压缩不划算&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;3&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;gzip_comp_level &lt;/span&gt;&lt;span&gt;4&lt;/span&gt;&lt;span&gt;;           &lt;/span&gt;&lt;span&gt;# 1-9，超过 5 收益很小、CPU 开销明显&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;4&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;gzip_types &lt;/span&gt;&lt;span&gt;text/plain text/css application/javascript application/json application/xml;&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;5&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;gzip_vary &lt;/span&gt;&lt;span&gt;on&lt;/span&gt;&lt;span&gt;;                &lt;/span&gt;&lt;span&gt;# 加 Vary: Accept-Encoding，避免缓存把压缩版给不支持的客户端&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;/code&gt;&lt;/pre&gt;&lt;div&gt;&lt;div&gt;&lt;/div&gt;&lt;div&gt;&lt;/div&gt;&lt;/div&gt;&lt;/figure&gt;&lt;/div&gt;&lt;/section&gt;
&lt;section&gt;&lt;h2&gt;三、性能调优：一次简单的压测&lt;a href=&quot;#三性能调优一次简单的压测&quot;&gt;&lt;span&gt;#&lt;/span&gt;&lt;/a&gt;&lt;/h2&gt;&lt;p&gt;2018 年在一台 4 核 2G 的虚拟机上，用 webbench 对一个静态文件做过压测。Nginx 关键配置：&lt;/p&gt;&lt;div&gt;&lt;figure&gt;&lt;figcaption&gt;&lt;/figcaption&gt;&lt;pre&gt;&lt;code&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;1&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;worker_processes &lt;/span&gt;&lt;span&gt; auto;      &lt;/span&gt;&lt;span&gt;# 等于 CPU 核数，当时手写的是 4&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;2&lt;/div&gt;&lt;/div&gt;&lt;div&gt;
&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;3&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;events&lt;/span&gt;&lt;span&gt; {&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;4&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;   &lt;span&gt; &lt;/span&gt;&lt;/span&gt;&lt;span&gt;worker_connections &lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;1024&lt;/span&gt;&lt;span&gt;;  &lt;/span&gt;&lt;span&gt;# 单个 worker 的最大连接数&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;5&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;}&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;/code&gt;&lt;/pre&gt;&lt;div&gt;&lt;div&gt;&lt;/div&gt;&lt;div&gt;&lt;/div&gt;&lt;/div&gt;&lt;/figure&gt;&lt;/div&gt;

&lt;table&gt;&lt;thead&gt;&lt;tr&gt;&lt;th&gt;并发客户端&lt;/th&gt;&lt;th&gt;持续时间&lt;/th&gt;&lt;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;500&lt;/td&gt;&lt;td&gt;10 s&lt;/td&gt;&lt;td&gt;164,548&lt;/td&gt;&lt;td&gt;16,454&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;1,000&lt;/td&gt;&lt;td&gt;10 s&lt;/td&gt;&lt;td&gt;198,593&lt;/td&gt;&lt;td&gt;19,859&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;2,000&lt;/td&gt;&lt;td&gt;10 s&lt;/td&gt;&lt;td&gt;190,463&lt;/td&gt;&lt;td&gt;19,046&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;5,000&lt;/td&gt;&lt;td&gt;10 s&lt;/td&gt;&lt;td&gt;182,662&lt;/td&gt;&lt;td&gt;18,266&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;10,000&lt;/td&gt;&lt;td&gt;10 s&lt;/td&gt;&lt;td&gt;161,927&lt;/td&gt;&lt;td&gt;16,192&lt;/td&gt;&lt;/tr&gt;&lt;/tbody&gt;&lt;/table&gt;&lt;p&gt;几个结论：&lt;/p&gt;&lt;ul&gt;
&lt;li&gt;吞吐在 1000 并发左右到顶，约 2 万 QPS，再加并发反而下降。说明这台机器已经到了处理上限，继续加并发只会增加连接管理和调度的开销。&lt;/li&gt;
&lt;li&gt;&lt;code&gt;worker_processes × worker_connections&lt;/code&gt; 是理论最大连接数，并发超过它会直接报错；同时要调高系统的文件句柄限制（&lt;code&gt;ulimit -n&lt;/code&gt; 和 &lt;code&gt;worker_rlimit_nofile&lt;/code&gt;）。&lt;/li&gt;
&lt;li&gt;这只是静态小文件的结果。真实 CDN 节点上，磁盘 IO、TLS 握手、回源才是大头，不能用这个数字去估容量。&lt;/li&gt;
&lt;/ul&gt;&lt;p&gt;webbench 现在已经很少用了，同类压测更推荐 &lt;a href=&quot;https://github.com/wg/wrk&quot; target=&quot;_blank&quot;&gt;wrk&lt;/a&gt;，它支持 Lua 脚本构造请求。&lt;/p&gt;&lt;/section&gt;
&lt;section&gt;&lt;h2&gt;四、OpenResty 的请求处理阶段&lt;a href=&quot;#四openresty-的请求处理阶段&quot;&gt;&lt;span&gt;#&lt;/span&gt;&lt;/a&gt;&lt;/h2&gt;&lt;p&gt;Nginx 把一个请求的处理分成多个阶段，OpenResty 允许在每个阶段挂 Lua 代码。写定制逻辑之前，先想清楚它该放在哪个阶段：&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;rewrite&lt;/td&gt;&lt;td&gt;&lt;code&gt;rewrite_by_lua_block&lt;/code&gt;&lt;/td&gt;&lt;td&gt;改写 URI、参数&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;access&lt;/td&gt;&lt;td&gt;&lt;code&gt;access_by_lua_block&lt;/code&gt;&lt;/td&gt;&lt;td&gt;鉴权、防盗链、限流，不通过就 &lt;code&gt;ngx.exit(403)&lt;/code&gt;&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;content&lt;/td&gt;&lt;td&gt;&lt;code&gt;content_by_lua_block&lt;/code&gt;&lt;/td&gt;&lt;td&gt;直接生成响应（不走 proxy_pass 时）&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;header_filter&lt;/td&gt;&lt;td&gt;&lt;code&gt;header_filter_by_lua_block&lt;/code&gt;&lt;/td&gt;&lt;td&gt;改响应头，比如加 CORS、改缓存头&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;body_filter&lt;/td&gt;&lt;td&gt;&lt;code&gt;body_filter_by_lua_block&lt;/code&gt;&lt;/td&gt;&lt;td&gt;改响应体，流式处理&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;log&lt;/td&gt;&lt;td&gt;&lt;code&gt;log_by_lua_block&lt;/code&gt;&lt;/td&gt;&lt;td&gt;统计、打点，不影响响应时间&lt;/td&gt;&lt;/tr&gt;&lt;/tbody&gt;&lt;/table&gt;&lt;p&gt;在 CDN 节点上，客户定制几乎都落在 rewrite 和 access 两个阶段：请求进来，先改写、再鉴权，通过之后才查缓存或者回源。&lt;/p&gt;&lt;/section&gt;
&lt;section&gt;&lt;h2&gt;五、CDN 边缘的 Lua 定制实践&lt;a href=&quot;#五cdn-边缘的-lua-定制实践&quot;&gt;&lt;span&gt;#&lt;/span&gt;&lt;/a&gt;&lt;/h2&gt;&lt;p&gt;在百度云 CDN 时，我参与过 Nginx 层的客户定制开发。下面挑三类典型需求，用简化版代码说明原理（生产代码还要处理配置下发、异常和监控，这里省略）。&lt;/p&gt;&lt;section&gt;&lt;h3&gt;1. 防盗链：Referer 校验 + 带时效的签名 URL&lt;a href=&quot;#1-防盗链referer-校验--带时效的签名-url&quot;&gt;&lt;span&gt;#&lt;/span&gt;&lt;/a&gt;&lt;/h3&gt;&lt;p&gt;最简单的防盗链是 Nginx 自带的 Referer 白名单，但 Referer 可以伪造。更可靠的是&lt;strong&gt;签名 URL&lt;/strong&gt;：客户的业务服务器用密钥给 URL 签名并带上过期时间，CDN 节点用同一个密钥校验。&lt;/p&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;&lt;figure&gt;&lt;figcaption&gt;&lt;/figcaption&gt;&lt;pre&gt;&lt;code&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;1&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;location&lt;/span&gt;&lt;span&gt; /video/ {&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;2&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;    &lt;/span&gt;&lt;span&gt;access_by_lua_block&lt;/span&gt;&lt;span&gt; {&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;3&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;        &lt;/span&gt;&lt;span&gt;local&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;args&lt;/span&gt;&lt;span&gt; = &lt;/span&gt;&lt;span&gt;ngx&lt;/span&gt;&lt;span&gt;.req.&lt;/span&gt;&lt;span&gt;get_uri_args&lt;/span&gt;&lt;span&gt;()&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;4&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;        &lt;/span&gt;&lt;span&gt;local&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;t&lt;/span&gt;&lt;span&gt;, &lt;/span&gt;&lt;span&gt;sign&lt;/span&gt;&lt;span&gt; = &lt;/span&gt;&lt;span&gt;tonumber&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;args&lt;/span&gt;&lt;span&gt;.t), &lt;/span&gt;&lt;span&gt;args&lt;/span&gt;&lt;span&gt;.sign&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;5&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;        &lt;/span&gt;&lt;span&gt;if&lt;/span&gt;&lt;span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;not&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;/span&gt;&lt;span&gt;t&lt;/span&gt;&lt;span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;or&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;not&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;/span&gt;&lt;span&gt;sign&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;then&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;6&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;            &lt;/span&gt;&lt;span&gt;return&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;ngx&lt;/span&gt;&lt;span&gt;.&lt;/span&gt;&lt;span&gt;exit&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;ngx&lt;/span&gt;&lt;span&gt;.HTTP_FORBIDDEN)&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;7&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;        &lt;/span&gt;&lt;span&gt;end&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;8&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;        &lt;/span&gt;&lt;span&gt;-- 过期检查&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;9&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;        &lt;/span&gt;&lt;span&gt;if&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;t&lt;/span&gt;&lt;span&gt; &amp;lt; &lt;/span&gt;&lt;span&gt;ngx&lt;/span&gt;&lt;span&gt;.&lt;/span&gt;&lt;span&gt;time&lt;/span&gt;&lt;span&gt;() &lt;/span&gt;&lt;span&gt;then&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;10&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;            &lt;/span&gt;&lt;span&gt;return&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;ngx&lt;/span&gt;&lt;span&gt;.&lt;/span&gt;&lt;span&gt;exit&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;ngx&lt;/span&gt;&lt;span&gt;.HTTP_FORBIDDEN)&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;11&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;        &lt;/span&gt;&lt;span&gt;end&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;12&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;        &lt;/span&gt;&lt;span&gt;-- 签名 = md5(密钥 + 路径 + 过期时间)，密钥按域名配置下发&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;13&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;        &lt;/span&gt;&lt;span&gt;local&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;secret&lt;/span&gt;&lt;span&gt; = &lt;/span&gt;&lt;span&gt;ngx&lt;/span&gt;&lt;span&gt;.var.auth_secret&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;14&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;        &lt;/span&gt;&lt;span&gt;if&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;ngx&lt;/span&gt;&lt;span&gt;.&lt;/span&gt;&lt;span&gt;md5&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;secret&lt;/span&gt;&lt;span&gt; .. &lt;/span&gt;&lt;span&gt;ngx&lt;/span&gt;&lt;span&gt;.var.uri .. &lt;/span&gt;&lt;span&gt;t&lt;/span&gt;&lt;span&gt;) ~= &lt;/span&gt;&lt;span&gt;sign&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;then&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;15&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;            &lt;/span&gt;&lt;span&gt;return&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;ngx&lt;/span&gt;&lt;span&gt;.&lt;/span&gt;&lt;span&gt;exit&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;ngx&lt;/span&gt;&lt;span&gt;.HTTP_FORBIDDEN)&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;16&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;        &lt;/span&gt;&lt;span&gt;end&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;17&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;&lt;span&gt;    &lt;/span&gt;&lt;/span&gt;&lt;span&gt;}&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;18&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;   &lt;span&gt; &lt;/span&gt;&lt;/span&gt;&lt;span&gt;proxy_pass &lt;/span&gt;&lt;span&gt;http://origin;&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;19&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;}&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;/code&gt;&lt;/pre&gt;&lt;div&gt;&lt;div&gt;&lt;/div&gt;&lt;div&gt;&lt;/div&gt;&lt;/div&gt;&lt;/figure&gt;&lt;div&gt;&lt;/div&gt;&lt;/div&gt;&lt;span&gt;展开&lt;/span&gt;&lt;span&gt;收起&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;p&gt;注意：签名参数要从缓存 key 里去掉，否则同一个文件每个签名都是一份缓存，命中率会很难看。&lt;/p&gt;&lt;/section&gt;&lt;section&gt;&lt;h3&gt;2. URI 改写&lt;a href=&quot;#2-uri-改写&quot;&gt;&lt;span&gt;#&lt;/span&gt;&lt;/a&gt;&lt;/h3&gt;&lt;p&gt;比如客户把旧路径迁移到新路径，或者要把带版本号的 URL 归一成同一份缓存：&lt;/p&gt;&lt;div&gt;&lt;figure&gt;&lt;figcaption&gt;&lt;/figcaption&gt;&lt;pre&gt;&lt;code&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;1&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;rewrite_by_lua_block&lt;/span&gt;&lt;span&gt; {&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;2&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;    &lt;/span&gt;&lt;span&gt;-- /static/v123/app.js -&amp;gt; /static/app.js&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;3&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;    &lt;/span&gt;&lt;span&gt;local&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;uri&lt;/span&gt;&lt;span&gt;, &lt;/span&gt;&lt;span&gt;n&lt;/span&gt;&lt;span&gt; = &lt;/span&gt;&lt;span&gt;ngx&lt;/span&gt;&lt;span&gt;.re.&lt;/span&gt;&lt;span&gt;sub&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;ngx&lt;/span&gt;&lt;span&gt;.var.uri, &lt;/span&gt;&lt;span&gt;[[^/static/v\d+/]]&lt;/span&gt;&lt;span&gt;, &lt;/span&gt;&lt;span&gt;&quot;/static/&quot;&lt;/span&gt;&lt;span&gt;, &lt;/span&gt;&lt;span&gt;&quot;jo&quot;&lt;/span&gt;&lt;span&gt;)&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;4&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;    &lt;/span&gt;&lt;span&gt;if&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;n&lt;/span&gt;&lt;span&gt; &amp;gt; &lt;/span&gt;&lt;span&gt;0&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;then&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;5&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;        &lt;/span&gt;&lt;span&gt;ngx&lt;/span&gt;&lt;span&gt;.req.&lt;/span&gt;&lt;span&gt;set_uri&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;uri&lt;/span&gt;&lt;span&gt;)&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;6&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;    &lt;/span&gt;&lt;span&gt;end&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;7&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;}&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;/code&gt;&lt;/pre&gt;&lt;div&gt;&lt;div&gt;&lt;/div&gt;&lt;div&gt;&lt;/div&gt;&lt;/div&gt;&lt;/figure&gt;&lt;/div&gt;&lt;p&gt;&lt;code&gt;ngx.re&lt;/code&gt; 的 &lt;code&gt;&quot;jo&quot;&lt;/code&gt; 参数表示开启 PCRE JIT 并缓存编译后的正则，热点路径上性能差别很明显。&lt;/p&gt;&lt;/section&gt;&lt;section&gt;&lt;h3&gt;3. 回源 AWS S3 的 V4 签名鉴权&lt;a href=&quot;#3-回源-aws-s3-的-v4-签名鉴权&quot;&gt;&lt;span&gt;#&lt;/span&gt;&lt;/a&gt;&lt;/h3&gt;&lt;p&gt;当时有客户的源站是私有的 AWS S3 桶，CDN 回源时必须按 AWS Signature Version 4 签名，否则 S3 返回 403。这个功能做成了一个 Lua 插件，region、AccessKey、SecretKey、回源域名都按域名动态配置。&lt;/p&gt;&lt;p&gt;V4 签名的计算分四步：&lt;/p&gt;&lt;ol&gt;
&lt;li&gt;&lt;strong&gt;规范请求（Canonical Request）&lt;/strong&gt;：把方法、URI、查询参数、参与签名的请求头按固定格式拼起来；&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;待签字符串（String to Sign）&lt;/strong&gt;：算法名 + 时间 + 凭证范围（日期/region/s3/aws4_request）+ 规范请求的 SHA256；&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;派生签名密钥&lt;/strong&gt;：用 &lt;code&gt;&quot;AWS4&quot; + SecretKey&lt;/code&gt; 对日期、region、服务名、&lt;code&gt;aws4_request&lt;/code&gt; 逐层做 HMAC-SHA256；&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;签名&lt;/strong&gt;：用派生密钥对待签字符串做 HMAC-SHA256，结果写进 &lt;code&gt;Authorization&lt;/code&gt; 头。&lt;/li&gt;
&lt;/ol&gt;&lt;p&gt;简化后的 Lua 实现（HMAC 用的是 &lt;code&gt;lua-resty-hmac&lt;/code&gt; 库，回源只用 GET，所以 payload 用 &lt;code&gt;UNSIGNED-PAYLOAD&lt;/code&gt;）：&lt;/p&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;&lt;figure&gt;&lt;figcaption&gt;&lt;/figcaption&gt;&lt;pre&gt;&lt;code&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;1&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;local&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;resty_sha256&lt;/span&gt;&lt;span&gt; = &lt;/span&gt;&lt;span&gt;require&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;&quot;resty.sha256&quot;&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;2&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;local&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;str&lt;/span&gt;&lt;span&gt;  = &lt;/span&gt;&lt;span&gt;require&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;&quot;resty.string&quot;&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;3&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;local&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;hmac&lt;/span&gt;&lt;span&gt; = &lt;/span&gt;&lt;span&gt;require&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;&quot;resty.hmac&quot;&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;4&lt;/div&gt;&lt;/div&gt;&lt;div&gt;
&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;5&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;local&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;function&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;sha256_hex&lt;/span&gt;&lt;span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;s&lt;/span&gt;&lt;span&gt;)&lt;/span&gt;&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;6&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;    &lt;/span&gt;&lt;span&gt;local&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;h&lt;/span&gt;&lt;span&gt; = &lt;/span&gt;&lt;span&gt;resty_sha256&lt;/span&gt;&lt;span&gt;:&lt;/span&gt;&lt;span&gt;new&lt;/span&gt;&lt;span&gt;()&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;7&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;    &lt;/span&gt;&lt;span&gt;h&lt;/span&gt;&lt;span&gt;:&lt;/span&gt;&lt;span&gt;update&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;s&lt;/span&gt;&lt;span&gt;)&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;8&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;    &lt;/span&gt;&lt;span&gt;return&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;str&lt;/span&gt;&lt;span&gt;.&lt;/span&gt;&lt;span&gt;to_hex&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;h&lt;/span&gt;&lt;span&gt;:&lt;/span&gt;&lt;span&gt;final&lt;/span&gt;&lt;span&gt;())&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;9&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;end&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;10&lt;/div&gt;&lt;/div&gt;&lt;div&gt;
&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;11&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;local&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;function&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;hmac_sha256&lt;/span&gt;&lt;span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;key&lt;/span&gt;&lt;span&gt;, &lt;/span&gt;&lt;span&gt;msg&lt;/span&gt;&lt;span&gt;)&lt;/span&gt;&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;12&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;    &lt;/span&gt;&lt;span&gt;return&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;hmac&lt;/span&gt;&lt;span&gt;:&lt;/span&gt;&lt;span&gt;new&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;key&lt;/span&gt;&lt;span&gt;, &lt;/span&gt;&lt;span&gt;hmac&lt;/span&gt;&lt;span&gt;.ALGOS.SHA256):&lt;/span&gt;&lt;span&gt;final&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;msg&lt;/span&gt;&lt;span&gt;)  &lt;/span&gt;&lt;span&gt;-- 返回二进制&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;13&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;end&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;14&lt;/div&gt;&lt;/div&gt;&lt;div&gt;
&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;15&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;local&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;ak&lt;/span&gt;&lt;span&gt;, &lt;/span&gt;&lt;span&gt;sk&lt;/span&gt;&lt;span&gt;   = &lt;/span&gt;&lt;span&gt;ngx&lt;/span&gt;&lt;span&gt;.var.s3_ak, &lt;/span&gt;&lt;span&gt;ngx&lt;/span&gt;&lt;span&gt;.var.s3_sk&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;16&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;local&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;region&lt;/span&gt;&lt;span&gt;   = &lt;/span&gt;&lt;span&gt;ngx&lt;/span&gt;&lt;span&gt;.var.s3_region&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;17&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;local&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;host&lt;/span&gt;&lt;span&gt;     = &lt;/span&gt;&lt;span&gt;ngx&lt;/span&gt;&lt;span&gt;.var.s3_host       &lt;/span&gt;&lt;span&gt;-- bucket.s3.&amp;lt;region&amp;gt;.amazonaws.com&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;18&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;local&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;amz_date&lt;/span&gt;&lt;span&gt; = &lt;/span&gt;&lt;span&gt;os.date&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;&quot;!%Y%m%dT%H%M%SZ&quot;&lt;/span&gt;&lt;span&gt;, &lt;/span&gt;&lt;span&gt;ngx&lt;/span&gt;&lt;span&gt;.&lt;/span&gt;&lt;span&gt;time&lt;/span&gt;&lt;span&gt;())&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;19&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;local&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;date&lt;/span&gt;&lt;span&gt;     = &lt;/span&gt;&lt;span&gt;amz_date&lt;/span&gt;&lt;span&gt;:&lt;/span&gt;&lt;span&gt;sub&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;1&lt;/span&gt;&lt;span&gt;, &lt;/span&gt;&lt;span&gt;8&lt;/span&gt;&lt;span&gt;)&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;20&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;local&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;payload&lt;/span&gt;&lt;span&gt;  = &lt;/span&gt;&lt;span&gt;&quot;UNSIGNED-PAYLOAD&quot;&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;21&lt;/div&gt;&lt;/div&gt;&lt;div&gt;
&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;22&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;-- 1. 规范请求（URI 需按 AWS 规则逐段编码，查询参数需排序，这里省略）&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;23&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;local&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;signed_headers&lt;/span&gt;&lt;span&gt; = &lt;/span&gt;&lt;span&gt;&quot;host;x-amz-content-sha256;x-amz-date&quot;&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;24&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;local&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;canonical&lt;/span&gt;&lt;span&gt; = &lt;/span&gt;&lt;span&gt;table.concat&lt;/span&gt;&lt;span&gt;({&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;25&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;    &lt;/span&gt;&lt;span&gt;&quot;GET&quot;&lt;/span&gt;&lt;span&gt;,&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;26&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;    &lt;/span&gt;&lt;span&gt;ngx&lt;/span&gt;&lt;span&gt;.var.uri,&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;27&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;    &lt;/span&gt;&lt;span&gt;ngx&lt;/span&gt;&lt;span&gt;&lt;span&gt;.var.args &lt;/span&gt;&lt;span&gt;or&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;/span&gt;&lt;span&gt;&quot;&quot;&lt;/span&gt;&lt;span&gt;,&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;28&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;    &lt;/span&gt;&lt;span&gt;&quot;host:&quot; &lt;/span&gt;&lt;span&gt;.. &lt;/span&gt;&lt;span&gt;host&lt;/span&gt;&lt;span&gt; .. &lt;/span&gt;&lt;span&gt;&quot;&lt;/span&gt;&lt;span&gt;\n&lt;/span&gt;&lt;span&gt;x-amz-content-sha256:&quot; &lt;/span&gt;&lt;span&gt;.. &lt;/span&gt;&lt;span&gt;payload&lt;/span&gt;&lt;span&gt; .. &lt;/span&gt;&lt;span&gt;&quot;&lt;/span&gt;&lt;span&gt;\n&lt;/span&gt;&lt;span&gt;x-amz-date:&quot; &lt;/span&gt;&lt;span&gt;.. &lt;/span&gt;&lt;span&gt;amz_date&lt;/span&gt;&lt;span&gt; .. &lt;/span&gt;&lt;span&gt;&quot;&lt;/span&gt;&lt;span&gt;\n&lt;/span&gt;&lt;span&gt;&quot;&lt;/span&gt;&lt;span&gt;,&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;29&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;    &lt;/span&gt;&lt;span&gt;signed_headers&lt;/span&gt;&lt;span&gt;,&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;30&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;    &lt;/span&gt;&lt;span&gt;payload&lt;/span&gt;&lt;span&gt;,&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;31&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;}, &lt;/span&gt;&lt;span&gt;&quot;&lt;/span&gt;&lt;span&gt;\n&lt;/span&gt;&lt;span&gt;&quot;&lt;/span&gt;&lt;span&gt;)&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;32&lt;/div&gt;&lt;/div&gt;&lt;div&gt;
&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;33&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;-- 2. 待签字符串&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;34&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;local&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;scope&lt;/span&gt;&lt;span&gt; = &lt;/span&gt;&lt;span&gt;date&lt;/span&gt;&lt;span&gt; .. &lt;/span&gt;&lt;span&gt;&quot;/&quot; &lt;/span&gt;&lt;span&gt;.. &lt;/span&gt;&lt;span&gt;region&lt;/span&gt;&lt;span&gt; .. &lt;/span&gt;&lt;span&gt;&quot;/s3/aws4_request&quot;&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;35&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;local&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;string_to_sign&lt;/span&gt;&lt;span&gt; = &lt;/span&gt;&lt;span&gt;&quot;AWS4-HMAC-SHA256&lt;/span&gt;&lt;span&gt;\n&lt;/span&gt;&lt;span&gt;&quot; &lt;/span&gt;&lt;span&gt;.. &lt;/span&gt;&lt;span&gt;amz_date&lt;/span&gt;&lt;span&gt; .. &lt;/span&gt;&lt;span&gt;&quot;&lt;/span&gt;&lt;span&gt;\n&lt;/span&gt;&lt;span&gt;&quot; &lt;/span&gt;&lt;span&gt;.. &lt;/span&gt;&lt;span&gt;scope&lt;/span&gt;&lt;span&gt; .. &lt;/span&gt;&lt;span&gt;&quot;&lt;/span&gt;&lt;span&gt;\n&lt;/span&gt;&lt;span&gt;&quot; &lt;/span&gt;&lt;span&gt;.. &lt;/span&gt;&lt;span&gt;sha256_hex&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;canonical&lt;/span&gt;&lt;span&gt;)&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;36&lt;/div&gt;&lt;/div&gt;&lt;div&gt;
&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;37&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;-- 3. 派生签名密钥&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;38&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;local&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;k&lt;/span&gt;&lt;span&gt; = &lt;/span&gt;&lt;span&gt;hmac_sha256&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;&quot;AWS4&quot; &lt;/span&gt;&lt;span&gt;.. &lt;/span&gt;&lt;span&gt;sk&lt;/span&gt;&lt;span&gt;, &lt;/span&gt;&lt;span&gt;date&lt;/span&gt;&lt;span&gt;)&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;39&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;k&lt;/span&gt;&lt;span&gt; = &lt;/span&gt;&lt;span&gt;hmac_sha256&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;k&lt;/span&gt;&lt;span&gt;, &lt;/span&gt;&lt;span&gt;region&lt;/span&gt;&lt;span&gt;)&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;40&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;k&lt;/span&gt;&lt;span&gt; = &lt;/span&gt;&lt;span&gt;hmac_sha256&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;k&lt;/span&gt;&lt;span&gt;, &lt;/span&gt;&lt;span&gt;&quot;s3&quot;&lt;/span&gt;&lt;span&gt;)&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;41&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;k&lt;/span&gt;&lt;span&gt; = &lt;/span&gt;&lt;span&gt;hmac_sha256&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;k&lt;/span&gt;&lt;span&gt;, &lt;/span&gt;&lt;span&gt;&quot;aws4_request&quot;&lt;/span&gt;&lt;span&gt;)&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;42&lt;/div&gt;&lt;/div&gt;&lt;div&gt;
&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;43&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;-- 4. 签名并设置回源请求头&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;44&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;local&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;signature&lt;/span&gt;&lt;span&gt; = &lt;/span&gt;&lt;span&gt;str&lt;/span&gt;&lt;span&gt;.&lt;/span&gt;&lt;span&gt;to_hex&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;hmac_sha256&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;k&lt;/span&gt;&lt;span&gt;, &lt;/span&gt;&lt;span&gt;string_to_sign&lt;/span&gt;&lt;span&gt;))&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;45&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;ngx&lt;/span&gt;&lt;span&gt;.req.&lt;/span&gt;&lt;span&gt;set_header&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;&quot;x-amz-date&quot;&lt;/span&gt;&lt;span&gt;, &lt;/span&gt;&lt;span&gt;amz_date&lt;/span&gt;&lt;span&gt;)&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;46&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;ngx&lt;/span&gt;&lt;span&gt;.req.&lt;/span&gt;&lt;span&gt;set_header&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;&quot;x-amz-content-sha256&quot;&lt;/span&gt;&lt;span&gt;, &lt;/span&gt;&lt;span&gt;payload&lt;/span&gt;&lt;span&gt;)&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;47&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;ngx&lt;/span&gt;&lt;span&gt;.req.&lt;/span&gt;&lt;span&gt;set_header&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;&quot;Authorization&quot;&lt;/span&gt;&lt;span&gt;, &lt;/span&gt;&lt;span&gt;&quot;AWS4-HMAC-SHA256 Credential=&quot; &lt;/span&gt;&lt;span&gt;.. &lt;/span&gt;&lt;span&gt;ak&lt;/span&gt;&lt;span&gt; .. &lt;/span&gt;&lt;span&gt;&quot;/&quot; &lt;/span&gt;&lt;span&gt;.. &lt;/span&gt;&lt;span&gt;scope&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;48&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;&lt;span&gt;    &lt;/span&gt;&lt;/span&gt;&lt;span&gt;.. &lt;/span&gt;&lt;span&gt;&quot;, SignedHeaders=&quot; &lt;/span&gt;&lt;span&gt;.. &lt;/span&gt;&lt;span&gt;signed_headers&lt;/span&gt;&lt;span&gt; .. &lt;/span&gt;&lt;span&gt;&quot;, Signature=&quot; &lt;/span&gt;&lt;span&gt;.. &lt;/span&gt;&lt;span&gt;signature&lt;/span&gt;&lt;span&gt;)&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;/code&gt;&lt;/pre&gt;&lt;div&gt;&lt;div&gt;&lt;/div&gt;&lt;div&gt;&lt;/div&gt;&lt;/div&gt;&lt;/figure&gt;&lt;div&gt;&lt;/div&gt;&lt;/div&gt;&lt;span&gt;展开&lt;/span&gt;&lt;span&gt;收起&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;p&gt;配合的 Nginx 配置要注意两点：回源的 &lt;code&gt;Host&lt;/code&gt; 必须和签名里的一致，所以要 &lt;code&gt;proxy_set_header Host $s3_host&lt;/code&gt;；另外 S3 只接受和服务端时间相差 15 分钟以内的签名，节点时钟一旦漂移，就会出现间歇性 403，排查起来很隐蔽。&lt;/p&gt;&lt;/section&gt;&lt;/section&gt;
&lt;section&gt;&lt;h2&gt;六、调试 Lua&lt;a href=&quot;#六调试-lua&quot;&gt;&lt;span&gt;#&lt;/span&gt;&lt;/a&gt;&lt;/h2&gt;&lt;p&gt;线上排查主要靠 &lt;code&gt;ngx.log(ngx.ERR, ...)&lt;/code&gt; 打日志，配合 &lt;code&gt;error_log&lt;/code&gt; 的级别控制。本地开发想单步调试，可以用 &lt;a href=&quot;https://studio.zerobrane.com/&quot; target=&quot;_blank&quot;&gt;ZeroBrane Studio&lt;/a&gt;，它支持远程调试 OpenResty 里的 Lua 代码；在没有图形界面的 Linux 服务器上，可以通过 &lt;code&gt;ssh -X&lt;/code&gt; 把界面转发到本机。&lt;/p&gt;&lt;/section&gt;
&lt;section&gt;&lt;h2&gt;参考&lt;a href=&quot;#参考&quot;&gt;&lt;span&gt;#&lt;/span&gt;&lt;/a&gt;&lt;/h2&gt;&lt;ul&gt;
&lt;li&gt;&lt;a href=&quot;https://github.com/openresty/lua-nginx-module&quot; target=&quot;_blank&quot;&gt;OpenResty 官方文档 lua-nginx-module&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://docs.aws.amazon.com/AmazonS3/latest/API/sig-v4-header-based-auth.html&quot; target=&quot;_blank&quot;&gt;AWS Signature Version 4 签名流程&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://github.com/jkeys089/lua-resty-hmac&quot; target=&quot;_blank&quot;&gt;lua-resty-hmac&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;&lt;/section&gt;</content:encoded></item><item><title>一台电脑同时使用 GitHub 和公司 GitLab</title><link>https://tracerzzz.cn/posts/gitlab-and-github/</link><guid isPermaLink="true">https://tracerzzz.cn/posts/gitlab-and-github/</guid><description>个人项目用 GitHub、公司项目用 GitLab 时，如何在同一台电脑上配置两套 SSH 密钥和两套提交身份，并且按目录自动切换，不用每个仓库手动设置。</description><pubDate>Tue, 11 Jul 2017 16:00:00 GMT</pubDate><content:encoded>&lt;p&gt;个人项目在 GitHub，公司项目在 GitLab，两边的账号、邮箱都不一样。要解决两个问题：&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;&lt;strong&gt;认证&lt;/strong&gt;：两边用不同的 SSH 密钥；&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;身份&lt;/strong&gt;：提交记录里的用户名和邮箱，个人项目用个人的，公司项目用公司的。&lt;/li&gt;
&lt;/ol&gt;
&lt;section&gt;&lt;h2&gt;一、生成两套 SSH 密钥&lt;a href=&quot;#一生成两套-ssh-密钥&quot;&gt;&lt;span&gt;#&lt;/span&gt;&lt;/a&gt;&lt;/h2&gt;&lt;p&gt;现在推荐用 ed25519 算法，比 RSA 更短更安全：&lt;/p&gt;&lt;div&gt;&lt;figure&gt;&lt;figcaption&gt;&lt;span&gt;&lt;/span&gt;&lt;span&gt;Terminal window&lt;/span&gt;&lt;/figcaption&gt;&lt;pre&gt;&lt;code&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;1&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;ssh-keygen&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;-t&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;ed25519&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;-C&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;&quot;个人邮箱&quot;&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;-f&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;~/.ssh/id_ed25519_github&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;2&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;ssh-keygen&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;-t&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;ed25519&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;-C&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;&quot;公司邮箱&quot;&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;-f&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;~/.ssh/id_ed25519_gitlab&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;/code&gt;&lt;/pre&gt;&lt;div&gt;&lt;div&gt;&lt;/div&gt;&lt;div&gt;&lt;/div&gt;&lt;/div&gt;&lt;/figure&gt;&lt;/div&gt;&lt;p&gt;然后把两个 &lt;code&gt;.pub&lt;/code&gt; 公钥分别添加到 GitHub 和 GitLab 的个人设置里。&lt;/p&gt;&lt;/section&gt;
&lt;section&gt;&lt;h2&gt;二、用 ~/.ssh/config 按域名选择密钥&lt;a href=&quot;#二用-sshconfig-按域名选择密钥&quot;&gt;&lt;span&gt;#&lt;/span&gt;&lt;/a&gt;&lt;/h2&gt;&lt;div&gt;&lt;figure&gt;&lt;figcaption&gt;&lt;/figcaption&gt;&lt;pre&gt;&lt;code&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;1&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;# 个人 GitHub&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;2&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;Host&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;github.com&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;3&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;    &lt;/span&gt;&lt;span&gt;HostName&lt;/span&gt;&lt;span&gt; github.com&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;4&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;    &lt;/span&gt;&lt;span&gt;User&lt;/span&gt;&lt;span&gt; git&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;5&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;    &lt;/span&gt;&lt;span&gt;IdentityFile&lt;/span&gt;&lt;span&gt; ~/.ssh/id_ed25519_github&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;6&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;    &lt;/span&gt;&lt;span&gt;IdentitiesOnly&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;yes&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;7&lt;/div&gt;&lt;/div&gt;&lt;div&gt;
&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;8&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;# 公司 GitLab&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;9&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;Host&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;gitlab.example.com&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;10&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;    &lt;/span&gt;&lt;span&gt;HostName&lt;/span&gt;&lt;span&gt; gitlab.example.com&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;11&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;    &lt;/span&gt;&lt;span&gt;User&lt;/span&gt;&lt;span&gt; git&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;12&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;    &lt;/span&gt;&lt;span&gt;IdentityFile&lt;/span&gt;&lt;span&gt; ~/.ssh/id_ed25519_gitlab&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;13&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;    &lt;/span&gt;&lt;span&gt;IdentitiesOnly&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;yes&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;/code&gt;&lt;/pre&gt;&lt;div&gt;&lt;div&gt;&lt;/div&gt;&lt;div&gt;&lt;/div&gt;&lt;/div&gt;&lt;/figure&gt;&lt;/div&gt;&lt;p&gt;&lt;code&gt;IdentitiesOnly yes&lt;/code&gt; 很重要：没有它，SSH 会先把 ssh-agent 里所有的密钥挨个试一遍，可能用错密钥，或者尝试次数过多被服务器断开。&lt;/p&gt;&lt;p&gt;配好后验证：&lt;/p&gt;&lt;div&gt;&lt;figure&gt;&lt;figcaption&gt;&lt;span&gt;&lt;/span&gt;&lt;span&gt;Terminal window&lt;/span&gt;&lt;/figcaption&gt;&lt;pre&gt;&lt;code&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;1&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;ssh&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;-T&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;git@github.com&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;2&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;ssh&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;-T&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;git@gitlab.example.com&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;/code&gt;&lt;/pre&gt;&lt;div&gt;&lt;div&gt;&lt;/div&gt;&lt;div&gt;&lt;/div&gt;&lt;/div&gt;&lt;/figure&gt;&lt;/div&gt;&lt;/section&gt;
&lt;section&gt;&lt;h2&gt;三、按目录自动切换提交身份&lt;a href=&quot;#三按目录自动切换提交身份&quot;&gt;&lt;span&gt;#&lt;/span&gt;&lt;/a&gt;&lt;/h2&gt;&lt;p&gt;当年的做法是在每个公司仓库里手动执行 &lt;code&gt;git config --local user.name/user.email&lt;/code&gt;，很容易忘。Git 2.13 之后可以用 &lt;code&gt;includeIf&lt;/code&gt;，按仓库所在目录自动加载不同的配置。&lt;/p&gt;&lt;p&gt;&lt;code&gt;~/.gitconfig&lt;/code&gt;：&lt;/p&gt;&lt;div&gt;&lt;figure&gt;&lt;figcaption&gt;&lt;/figcaption&gt;&lt;pre&gt;&lt;code&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;1&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;[user]&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;2&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;    &lt;/span&gt;&lt;span&gt;name&lt;/span&gt;&lt;span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;=&lt;/span&gt;&lt;span&gt; 个人用户名&lt;/span&gt;&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;3&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;    &lt;/span&gt;&lt;span&gt;email&lt;/span&gt;&lt;span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;=&lt;/span&gt;&lt;span&gt; 个人邮箱&lt;/span&gt;&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;4&lt;/div&gt;&lt;/div&gt;&lt;div&gt;
&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;5&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;# work 目录下的仓库，自动使用公司身份&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;6&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;[includeIf &quot;gitdir:~/work/&quot;]&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;7&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;    &lt;/span&gt;&lt;span&gt;path&lt;/span&gt;&lt;span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;=&lt;/span&gt;&lt;span&gt; ~/.gitconfig-work&lt;/span&gt;&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;/code&gt;&lt;/pre&gt;&lt;div&gt;&lt;div&gt;&lt;/div&gt;&lt;div&gt;&lt;/div&gt;&lt;/div&gt;&lt;/figure&gt;&lt;/div&gt;&lt;p&gt;&lt;code&gt;~/.gitconfig-work&lt;/code&gt;：&lt;/p&gt;&lt;div&gt;&lt;figure&gt;&lt;figcaption&gt;&lt;/figcaption&gt;&lt;pre&gt;&lt;code&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;1&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;[user]&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;2&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;    &lt;/span&gt;&lt;span&gt;name&lt;/span&gt;&lt;span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;=&lt;/span&gt;&lt;span&gt; 公司用户名&lt;/span&gt;&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;3&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;    &lt;/span&gt;&lt;span&gt;email&lt;/span&gt;&lt;span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;=&lt;/span&gt;&lt;span&gt; 公司邮箱&lt;/span&gt;&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;/code&gt;&lt;/pre&gt;&lt;div&gt;&lt;div&gt;&lt;/div&gt;&lt;div&gt;&lt;/div&gt;&lt;/div&gt;&lt;/figure&gt;&lt;/div&gt;&lt;p&gt;公司的项目都放在 &lt;code&gt;~/work/&lt;/code&gt; 下面，提交时就会自动用公司身份。在仓库里执行 &lt;code&gt;git config user.email&lt;/code&gt; 可以确认当前生效的是哪个。&lt;/p&gt;&lt;/section&gt;
&lt;section&gt;&lt;h2&gt;四、遇到过的问题&lt;a href=&quot;#四遇到过的问题&quot;&gt;&lt;span&gt;#&lt;/span&gt;&lt;/a&gt;&lt;/h2&gt;&lt;p&gt;当时推送到公司 GitLab 时出现过这个报错：&lt;/p&gt;&lt;div&gt;&lt;figure&gt;&lt;figcaption&gt;&lt;/figcaption&gt;&lt;pre&gt;&lt;code&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;1&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;ssh_exchange_identification: read: Connection reset by peer&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;2&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;fatal: Could not read from remote repository.&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;/code&gt;&lt;/pre&gt;&lt;div&gt;&lt;div&gt;&lt;/div&gt;&lt;div&gt;&lt;/div&gt;&lt;/div&gt;&lt;/figure&gt;&lt;/div&gt;&lt;p&gt;它的意思是 SSH 连接在握手阶段就被服务端断开了，还没轮到验证密钥，所以和密钥配置无关。常见原因是服务端限制了允许 SSH 的来源 IP（比如只放行公司内网或 VPN），或者短时间内连接次数过多被拦截。当时的临时办法是改用 HTTPS 地址推送；根本的解决办法是找管理员确认 SSH 访问策略。&lt;/p&gt;&lt;/section&gt;</content:encoded></item></channel></rss>