<?xml version="1.0" encoding="UTF-8"?><rss version="2.0" xmlns:content="http://purl.org/rss/1.0/modules/content/"><channel><title>Puji&apos;s Blog</title><description>Programming, systems, and design.</description><link>https://puji4810.github.io/</link><language>en</language><item><title>一个调度器的设计</title><link>https://puji4810.github.io/2026/07/26/A-scheduler-design/</link><guid isPermaLink="true">https://puji4810.github.io/2026/07/26/A-scheduler-design/</guid><description>本文讨论 Prism 存储引擎调度系统的演化与现行实现：中央派发的结构性代价、执行资源的物理隔离、worker-local 提交与工作窃取，以及这些机制如何与协程、I/O、写入排序、读视图和 compaction 生命周期共同构成一套可验证的并发架构。</description><pubDate>Sun, 26 Jul 2026 00:00:00 GMT</pubDate><content:encoded>&lt;p&gt;调度器处理的是工作与执行资源之间的映射：给定一组待执行任务，决定它们应当进入哪一个执行域、由哪一个线程取得，以及完成后应当在哪里继续执行。对于存储系统而言，这并非一个独立于业务语义的通用问题。点读、WAL 写入、文件元数据操作与 compaction 对顺序、阻塞和并行度的要求各不相同；若将它们置于同一资源池中，调度策略便会无意间改变存储层的延迟分布，甚至承担本应由正确性协议表达的顺序约束。&lt;/p&gt;
&lt;p&gt;因此，Prism 的调度架构首先区分两类性质。第一类是&lt;strong&gt;资源性质&lt;/strong&gt;：一项工作会占用 CPU，还是可能在同步 I/O 或锁等待中阻塞。第二类是&lt;strong&gt;语义性质&lt;/strong&gt;：工作之间是否允许重排，是否要求单写者，以及取消或关机后仍需完成哪些副作用。本文将相同资源与语义约束下的一组执行资源称为一条 &lt;strong&gt;lane（执行通道）&lt;/strong&gt;。&lt;/p&gt;
&lt;p&gt;本文讨论 Prism 调度系统的三次主要重构。前半部分说明中央派发为何成为结构性瓶颈、物理隔离与 worker-local 队列分别解决了什么问题，以及两次失败的归因如何修正设计判断。后半部分以当前代码为依据，分析 &lt;code&gt;AsyncRuntime&lt;/code&gt;、三类执行器、协程 awaitable、io_uring reactor、写协调器、发布式读视图与 compaction 控制器之间的关系。&lt;/p&gt;

&lt;h1&gt;第一部分 · 从中央派发到物理隔离&lt;/h1&gt;
&lt;p&gt;本部分涉及多个历史版本。代码片段与测量数据描述的是相应改动发生时的实现，不应直接视为当前工作树的接口规格。第二部分则以当前提交为观察截面；这种区分十分必要，因为部分实验性机制后来已经被删除、回退或重新实现。&lt;/p&gt;
&lt;h2&gt;1. 初始设计：中央提交路径的代价&lt;/h2&gt;
&lt;p&gt;第一版调度器名为 &lt;code&gt;ThreadPoolScheduler&lt;/code&gt;。其拓扑并非最朴素的&quot;单一全局队列加一把互斥量&quot;，而是：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;N 个 worker 线程，&lt;strong&gt;每个 worker 持有私有 deque 和私有互斥量&lt;/strong&gt;&lt;/li&gt;
&lt;li&gt;一条分发线程处理全局优先队列&lt;/li&gt;
&lt;li&gt;一条分发线程处理延时任务&lt;/li&gt;
&lt;li&gt;一个 &lt;code&gt;pending_list_&lt;/code&gt; 登记当前空闲的 worker&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;提交路径如下：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;void ThreadPoolScheduler::Submit(Job job, std::size_t priority) {
    { std::lock_guard lock(priority_mutex_);
      priority_queue_.push(PriorityTask{ std::move(job), priority }); }
    priority_waiter_.release();
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;所有前台任务都必须经过这条路径，完整链路是：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;Submit()
  → priority_mutex_ + priority_queue_          （全局锁 #1）
  → priority_waiter_.release()                 （semaphore 唤醒）
  → 分发线程醒来                                 （线程切换 #1）
  → TryDispatch() → pending_mutex_             （全局锁 #2）
  → worker-&amp;gt;PushDispatched() → worker mutex    （锁 #3）
  → worker semaphore                           （线程切换 #2）
  → worker 执行完毕，回 pending_mutex_ 重新登记    （锁 #4）
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;一次提交需要获取四把互斥量、经历两次线程切换。&lt;/p&gt;
&lt;p&gt;该拓扑的关键问题在于，私有队列只降低了消费端的竞争。每个 worker 从本地 deque 取任务时不与其他 worker 争用，但所有生产者仍需依次经过 &lt;code&gt;priority_mutex_&lt;/code&gt; 与单线程分发器。因此，判断调度器的竞争程度不能只观察任务最终存放在哪里，还必须考察生产者抵达该位置之前经过的共享路径。第一版实现的最窄处正位于提交端。&lt;/p&gt;
&lt;p&gt;该版本还存在若干独立缺陷：&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;作业表示&lt;/strong&gt;。&lt;code&gt;using Job = std::function&amp;lt;void()&amp;gt;&lt;/code&gt; 意味着每个超出小对象优化容量的可调用对象都要一次堆分配。在每秒百万级提交的场景下，这是可观测的开销。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;亲和句柄缺乏实例身份&lt;/strong&gt;。&lt;code&gt;Context&lt;/code&gt; 仅保存 &lt;code&gt;std::thread::id&lt;/code&gt;，因而 &lt;code&gt;SubmitIn()&lt;/code&gt; 必须 O(N) 线性扫描以匹配线程。更严重的是，该句柄不携带&quot;它属于哪个调度器实例&quot;的信息——把一个池子捕获的 context 传给另一个池子会静默地索引到错误的 worker。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;派发失败时的作业丢失&lt;/strong&gt;。&lt;code&gt;TryDispatch(Job job)&lt;/code&gt; 按值取参，因此在派发失败的分支上，作业已经被移动走了，队列中残留一个被移动后的空壳。修正分两步完成：先把调用移入临界区，再把签名改为 &lt;code&gt;TryDispatch(Job&amp;amp;)&lt;/code&gt;，使其只在成功时消耗参数。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;析构不排空&lt;/strong&gt;。析构函数只 join 线程，队列中剩余的任务被直接丢弃。&lt;/p&gt;
&lt;h2&gt;2. 物理隔离：按阻塞性质拆分执行器&lt;/h2&gt;
&lt;p&gt;第一次改动没有更换队列算法，而是先改变执行资源的划分方式：不同阻塞性质与顺序要求的工作不再共享同一组 worker。&lt;/p&gt;
&lt;p&gt;改动前，一个 8 线程的执行器承担全部阻塞工作，compaction 与前台读共享同一条队列。在阻塞执行器上加入分段计时后可以看到量级差异：单次 compaction 执行约 &lt;strong&gt;1965 µs&lt;/strong&gt;，而单次点读约 &lt;strong&gt;0.5 µs&lt;/strong&gt;——后者仅占单次操作端到端耗时的 0.6%，其余时间用于排队与等待恢复。当毫秒级的任务排在微秒级的任务之前时，后者只能等待，这是典型的队头阻塞（head-of-line blocking）。&lt;/p&gt;
&lt;p&gt;由此可见，该阶段的主要延迟并非产生于点读本身，而是产生于点读与长时后台任务共享等待队列这一拓扑关系。&lt;/p&gt;
&lt;p&gt;隔离后形成六条物理执行源。每条 lane 的存在依据不同：&lt;/p&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Lane&lt;/th&gt;
&lt;th&gt;依据&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;DB 读（4 线程，开 stealing）&lt;/td&gt;
&lt;td&gt;&lt;code&gt;GetAsync&lt;/code&gt; 内部调用同步的 &lt;code&gt;DBImpl::Get&lt;/code&gt;，会遭遇 table 读、cache miss 与元数据锁，属于真实的阻塞工作&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;DB 写（单线程 FIFO）&lt;/td&gt;
&lt;td&gt;写顺序是正确性要求，单写者不变量比加锁更简单&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;阻塞 I/O（4 线程）&lt;/td&gt;
&lt;td&gt;&lt;code&gt;Env&lt;/code&gt; 元数据调用、reactor 无法处理的文件读、文件打开&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;有序文件（单线程 FIFO）&lt;/td&gt;
&lt;td&gt;保证 append/flush/sync/close 的提交顺序，属于正确性 lane 而非性能 lane&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Compaction（单线程，关闭 stealing）&lt;/td&gt;
&lt;td&gt;后台、阻塞、单飞语义；关闭 stealing 以防止它占用其他 lane 的 worker&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;CPU 池&lt;/td&gt;
&lt;td&gt;短续体、定时任务、reactor 读完成后的协程恢复&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;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;V1&lt;/td&gt;
&lt;td&gt;388,990&lt;/td&gt;
&lt;td&gt;1,138,247&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;2.93×&lt;/strong&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;V2&lt;/td&gt;
&lt;td&gt;506,948&lt;/td&gt;
&lt;td&gt;1,194,549&lt;/td&gt;
&lt;td&gt;2.36×&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;V3&lt;/td&gt;
&lt;td&gt;375,908&lt;/td&gt;
&lt;td&gt;1,128,880&lt;/td&gt;
&lt;td&gt;3.00×&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;V4&lt;/td&gt;
&lt;td&gt;440,652&lt;/td&gt;
&lt;td&gt;1,148,759&lt;/td&gt;
&lt;td&gt;2.61×&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;V5（10KB value，落盘）&lt;/td&gt;
&lt;td&gt;55,953&lt;/td&gt;
&lt;td&gt;773,567&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;13.83×&lt;/strong&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p&gt;上下文切换下降 80–89%（V1：1,137,827 → 145,765，−87.2%；V3：2,401,942 → 269,824，−88.8%）。CPU 效率指标为 &lt;strong&gt;指令数 −62%、cycles −67%、task-clock −45%、IPC +17%&lt;/strong&gt;。&lt;/p&gt;
&lt;p&gt;隔离本身的直接证据：前台读的平均排队等待从 38.6 µs 降至 &lt;strong&gt;21.7 µs&lt;/strong&gt;，compaction 的队列深度峰值为 &lt;strong&gt;0&lt;/strong&gt;——它不再在前台队列中排队。&lt;/p&gt;
&lt;p&gt;隔离后 CPU 利用率仍为 7.2%，低于预设的 25%。该指标覆盖了 prefill、compaction 与 teardown，而被测点读的 CPU 工作仅约 0.05 秒，因此它不能单独用于判断锁竞争是否仍然存在。这里保留这一未达标结果，是为了区分“指标未达到预期”与“机制没有生效”两个不同命题。&lt;/p&gt;
&lt;h2&gt;3. 短路前台：worker-local 快路径与工作窃取&lt;/h2&gt;
&lt;p&gt;物理隔离完成后，中央分发器仍然存在于前台路径上。此时的候选改动是把队列替换为无锁实现。&lt;/p&gt;
&lt;p&gt;在实施之前，先以不经过存储层的微基准测量从提交到执行的成本。该实验仅观察执行器本身：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;// All instrumentation is external — the BlockingExecutor class is UNMODIFIED.
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;结果：&lt;/p&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;配置&lt;/th&gt;
&lt;th&gt;队列深度峰值&lt;/th&gt;
&lt;th&gt;平均深度&lt;/th&gt;
&lt;th&gt;平均延迟&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;4 workers / 100k tasks&lt;/td&gt;
&lt;td&gt;11&lt;/td&gt;
&lt;td&gt;1.3&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;2.9 µs&lt;/strong&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;8 workers / 50k tasks&lt;/td&gt;
&lt;td&gt;7&lt;/td&gt;
&lt;td&gt;1.7&lt;/td&gt;
&lt;td&gt;3.7 µs&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;1 worker / 50k tasks&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;459&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;96.1&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;183.5 µs&lt;/strong&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p&gt;结果表明，在四个及以上 worker 的配置下，队列平均深度为 1.3，平均延迟不足 3 微秒，中央队列互斥量尚未饱和。因此，无锁队列并不是与证据相符的改造方向。真正需要消除的是前台任务进入中央派发链路这一事实。&lt;/p&gt;
&lt;h3&gt;3.1 快路径&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;template &amp;lt;typename F&amp;gt;
void SubmitJob(F&amp;amp;&amp;amp; job, std::size_t priority)
{
    assert(ShouldAcceptSubmitDuringShutdown());

    if (ShouldUseFastPath(priority))                       // POLICY: priority == 0
    {
        if (current_scheduler_ == this)                    // 自提交 → 回本 worker 队列
            if (TryEmplaceToWorker(std::forward&amp;lt;F&amp;gt;(job), current_worker_index_, false, true))
                return;

        const auto worker_index = ChooseSubmissionWorker();  // 外部提交 → round-robin
        if (TryEmplaceToWorker(std::forward&amp;lt;F&amp;gt;(job), worker_index, false, true))
            return;
    }
    PushToPriorityQueue(std::forward&amp;lt;F&amp;gt;(job), priority, /*wake=*/true);   // 回落
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;路径分两层。若调用者本身是该池的 worker（通过 &lt;code&gt;thread_local&lt;/code&gt; 判定），任务直接进入其自身队列，保留缓存局部性；否则以轮转方式选择一个 worker 直接推入。仅当两者都不适用时才回落到原有的优先队列。&lt;/p&gt;
&lt;p&gt;非零优先级、延时任务与关机残留仍走中央路径。保留该路径的依据有四点：它是低频优先级敏感工作的自然去处；它在直接路由不可用时提供安全兜底；它使非前台残留的关机排空更简单；它保持了延时任务提升语义的连续性。&lt;/p&gt;
&lt;h3&gt;3.2 被放弃的保证&lt;/h3&gt;
&lt;p&gt;该改动改变了调度器对顺序的承诺，因此需要明确记录其语义边界：&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;这是一次明确的交换：放弃普通前台提交之间的严格全局顺序，换取显著更低的提交与唤醒竞争。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;支撑该交换的论证是：现有测试断言的是完成性、亲和正确性、排空语义与失败快速行为，&lt;strong&gt;没有任何一条依赖跨 worker 的严格前台优先级顺序&lt;/strong&gt;。&lt;/p&gt;
&lt;p&gt;这种取舍必须成为接口契约的一部分。否则，后续维护者可能把非全序行为误判为实现缺陷，并重新引入中央串行化点。&lt;/p&gt;
&lt;h3&gt;3.3 工作窃取&lt;/h3&gt;
&lt;p&gt;引入 worker-local 队列后必须配套 stealing，否则负载分布会永久倾斜。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;std::size_t victim_index = NextRandom(rng_state) % (scheduler.work_threads_.size() - 1);
if (victim_index &amp;gt;= worker_index) ++victim_index;          // 排除自己，保持均匀

auto&amp;amp; victim = scheduler.work_threads_[victim_index];
std::scoped_lock lock(mutex_, victim.mutex_);              // 死锁安全的双锁
if (!queue_.empty()) return false;                         // 自身有任务则放弃

const std::size_t steal_count = std::max&amp;lt;std::size_t&amp;gt;(1, stealable_count / 2);
for (std::size_t i = victim.queue_.size(); i &amp;gt; 0 &amp;amp;&amp;amp; stolen.size() &amp;lt; steal_count; --i) { ... }
for (auto&amp;amp; queued : stolen) queue_.push_front(std::move(queued));
victim.load_.fetch_sub(stolen.size(), std::memory_order_relaxed);
load_.fetch_add(stolen.size(), std::memory_order_relaxed);
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;每个 worker 持有独立的 xorshift64 状态，以黄金比例常数按索引播种：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;std::uint64_t NextRandom(std::uint64_t&amp;amp; state) noexcept {
    state ^= state &amp;lt;&amp;lt; 13;  state ^= state &amp;gt;&amp;gt; 7;  state ^= state &amp;lt;&amp;lt; 17;  return state;
}
// rng_state = (worker_index + 1) * 0x9e3779b97f4a7c15ull;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;各项策略建立在以下约束之上：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;随机受害者而非扫描全部 worker&lt;/strong&gt;：扫描所有 worker 的紧循环会重新引入刚刚消除的竞争。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;从队尾偷取、每次偷取一半&lt;/strong&gt;：所有者从队首 FIFO 消费，保留自提交链的执行顺序直觉；从队尾取走可避开所有者的热点端；成批偷取比逐个偷取减少加锁次数。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;超时退避而非忙等&lt;/strong&gt;：允许 &lt;code&gt;try_acquire_for(100µs)&lt;/code&gt; 形式的定时重试，禁止自旋——否则等于用 CPU 自旋竞争替换互斥量竞争，总开销未必下降。&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;code&gt;std::scoped_lock lock(mutex_, victim.mutex_)&lt;/code&gt; 同时获取两把锁。此处必须使用 &lt;code&gt;scoped_lock&lt;/code&gt; 而非两个 &lt;code&gt;lock_guard&lt;/code&gt;：前者内部采用死锁避免算法，后者在两个 worker 互偷时会形成循环等待。&lt;/p&gt;
&lt;p&gt;实现过程中还暴露出一个活性缺口：worker 执行完偷取的一批任务后已经空闲，但旧判定依据的是出队前状态，因而没有重新登记。此时优先队列中仍有任务，而可用 worker 无法被分发器发现。修正后的判定在任务完成后重新检查 &lt;code&gt;queue_.empty()&lt;/code&gt;，并以专门的回归测试固定这一行为。该问题不会导致崩溃，只表现为特定混合负载下的饥饿。&lt;/p&gt;
&lt;h3&gt;3.4 测量结果&lt;/h3&gt;
&lt;p&gt;在固定的读密集混合负载下（48 客户端 / 24 worker / 1KB value），对照改造前的同一实现：&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;delta&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;吞吐&lt;/td&gt;
&lt;td&gt;716,367 ops/s&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;6,444,180 ops/s&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;+799.55%（9.00×）&lt;/strong&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;context switches&lt;/td&gt;
&lt;td&gt;2,224,436&lt;/td&gt;
&lt;td&gt;101,775&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;−95.42%&lt;/strong&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;task-clock&lt;/td&gt;
&lt;td&gt;27,707.88 msec&lt;/td&gt;
&lt;td&gt;15,089.20 msec&lt;/td&gt;
&lt;td&gt;−45.54%&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;wall time&lt;/td&gt;
&lt;td&gt;12.186 s&lt;/td&gt;
&lt;td&gt;9.717 s&lt;/td&gt;
&lt;td&gt;−20.26%&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;max_inflight&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;28&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;48&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;达到配置值&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;backend bound&lt;/td&gt;
&lt;td&gt;44.4 / 45.4%&lt;/td&gt;
&lt;td&gt;45.0 / 49.7%&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;pre&gt;&lt;code&gt;foreground_fastpath_submits = 1,200,049
foreground_fallback_submits = 0            ← 无一次回落
steal_attempts              = 1,250,321
steal_successes             = 50,248       ← 成功率 4%
worker_local_jobs_completed = 1,156,299
stolen_jobs_completed       = 43,750       ← 仅 3.6% 的任务被偷取过
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;code&gt;fallback_submits = 0&lt;/code&gt; 直接证明快路径在该负载下始终生效。stealing 成功率仅 4%，这正是设计预期：stealing 是负载失衡时的纠正机制，而非主要分发手段；若其成功率偏高，说明前台分发本身存在偏斜。&lt;/p&gt;
&lt;p&gt;上述结果比较的是同一异步实现改造前后的差异，而非异步实现与同步实现的差异。同期同步对照组未能形成有效样本，因此这里能够支持的结论仅限于“新调度路径相对于旧调度路径有所改善”。&lt;/p&gt;
&lt;h2&gt;4. 结构简化及其正确性代价&lt;/h2&gt;
&lt;p&gt;快路径落地后，调度器的可读性明显下降。随后的一轮工作目标是简化控制流，约束条件是 release benchmark 上不超过 5% 的性能回归。&lt;/p&gt;
&lt;h3&gt;4.1 以测试固定语义&lt;/h3&gt;
&lt;p&gt;第一步不修改任何逻辑，只在六个测试文件中补充特征测试，把当时的语义原样固定：优先级、延时任务在关机时的提升、context 亲和、异常安全、唤醒与推进、stealing。&lt;/p&gt;
&lt;p&gt;这是后续几次重构能够声称&quot;零行为变更&quot;的唯一依据。对并发组件而言，缺少语义基线的重构无法与引入回归区分。&lt;/p&gt;
&lt;h3&gt;4.2 策略与机制的分离&lt;/h3&gt;
&lt;p&gt;将分散在各提交路径中的布尔判定提取为具名谓词，将队列与线程操作提取为具名动作，并在头文件中以分隔线划分：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;// ── Policy helpers (path-selection decisions) ──────────────────
static bool ShouldUseFastPath(std::size_t priority) noexcept;
bool ShouldAcceptSubmitDuringShutdown() const noexcept;
static bool ShouldPromoteLazyTask(const LazyTask&amp;amp; task,
                                  std::chrono::steady_clock::time_point now) noexcept;

// ── Mechanics helpers (queue/thread operations) ─────────────────
void PushToPriorityQueue(Job job, std::size_t priority, bool wake = true);
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;调用点保留 &lt;code&gt;// POLICY:&lt;/code&gt; 与 &lt;code&gt;// MECHANICS&lt;/code&gt; 标注。若干&quot;解释单行代码在做什么&quot;的旧注释被删除——它们已被具名函数取代。&lt;/p&gt;
&lt;p&gt;同时明确了不提取的部分。&lt;code&gt;TrySteal&lt;/code&gt; 本身已是内聚的机械操作，&quot;是否应该偷&quot;就是调用它的控制流，额外包装一层谓词只增加间接层。消费循环的结构（出队 → 偷取 → 判定退出 → 退避）已足够清晰，追加标注会成为噪声。提取的判据是&quot;是否降低了理解成本&quot;，而非&quot;是否可以提取&quot;。&lt;/p&gt;
&lt;h3&gt;4.3 被回滚的临界区缩短&lt;/h3&gt;
&lt;p&gt;为缩短分发循环的临界区，把原有的&quot;锁内查看、派发成功后再弹出&quot;改为&quot;锁内弹出、锁外派发、失败后压回&quot;：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;PriorityTask task;
{
    std::lock_guard lock(priority_mutex_);
    if (priority_queue_.empty()) continue;
    task = std::move(const_cast&amp;lt;PriorityTask&amp;amp;&amp;gt;(priority_queue_.top()));
    priority_queue_.pop();                                   // 先弹出
}
if (!TryDispatch(task.job)) {
    PushToPriorityQueue(std::move(task.job), task.priority, /*wake=*/false);  // 失败压回
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;该改动在数十分钟后被回滚，因为它引入了两个正确性缺陷：&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;优先级反转&lt;/strong&gt;。任务离开队列到派发成功之间存在一个窗口，在此窗口内队列的偏序保证不成立。一个优先级更低的任务可以在此期间被取出并先行执行。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;同优先级轮转&lt;/strong&gt;。&lt;code&gt;std::priority_queue&lt;/code&gt; 对相同优先级元素不保证稳定序。反复&quot;弹出 → 派发失败 → 压回&quot;会使一批同优先级任务在堆中持续换位，部分任务被反复选中又反复放回，不收敛。&lt;/p&gt;
&lt;p&gt;此外还有一个未被立即察觉的后果：&lt;code&gt;wake=false&lt;/code&gt; 的重新入队不释放 semaphore。若此后不再有新的提交，分发线程不会被唤醒，任务将滞留至析构排空阶段。&lt;/p&gt;
&lt;p&gt;修正方式是颠倒顺序——&lt;strong&gt;先预约一个空闲 worker，再接触队列&lt;/strong&gt;，使派发必然成功，从而消除&quot;取出但未送出&quot;的中间状态：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;auto* worker = TryReserveIdleWorker();          // ① 先预约
if (worker == nullptr) continue;

PriorityTask task;  bool has_task = false;
{
    std::lock_guard lock(priority_mutex_);       // ② 再取任务
    if (!priority_queue_.empty()) { task = ...; priority_queue_.pop(); has_task = true; }
}
if (!has_task) { ReturnReservedIdleWorker(worker); continue; }   // ③ 空手则归还
worker-&amp;gt;PushDispatched(std::move(task.job));     // ④ 派发必成功
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;这一案例给出了临界区拆分的一般判据：&lt;strong&gt;能否拆分的标准不是&quot;这两步之间是否存在共享数据&quot;，而是&quot;中间状态对外可见时，不变量是否仍然成立&quot;&lt;/strong&gt;。优先队列的偏序不是队列内部的性质，而是&quot;取出&quot;与&quot;消费&quot;之间的性质；把这两步分开就等于把该性质拆掉。&lt;/p&gt;
&lt;p&gt;两个后续事实值得记录。其一，5% 的性能预算最终未被测量——在跑 release benchmark 之前，一个 compaction 测试失败，性能结论被标记为&quot;未测量&quot;而非默认通过。其二，为修复该缺陷引入的预约辅助函数在数周后成为死代码：分发循环第三次改写时放弃了预约模型，改为一次唤醒排空整个队列并直推负载最低的 worker。同一段约 30 行的循环前后被改写了四次。&lt;/p&gt;
&lt;h2&gt;5. 归因的陷阱：profiler 指向了错误的位置&lt;/h2&gt;
&lt;p&gt;某次改动后，异步 benchmark 的吞吐从 7–8M ops/s 降至 0.7–0.8M。复现稳定：&lt;/p&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;版本&lt;/th&gt;
&lt;th&gt;prefill=1&lt;/th&gt;
&lt;th&gt;prefill=0 复用&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;改动前&lt;/td&gt;
&lt;td&gt;8,175,611 ops/s&lt;/td&gt;
&lt;td&gt;8,203,663 ops/s&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;改动后&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;2,429,013 ops/s&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;2,413,994 ops/s&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p&gt;perf 数据高度一致地指向同步开销：&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;task-clock&lt;/td&gt;
&lt;td&gt;17,669 msec&lt;/td&gt;
&lt;td&gt;3,970 msec&lt;/td&gt;
&lt;td&gt;4.5×&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;sys time&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;5.65 s&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;0.59 s&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;9.6×&lt;/strong&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;sys %&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;32.4%&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;14.8%&lt;/td&gt;
&lt;td&gt;2.2×&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;instructions&lt;/td&gt;
&lt;td&gt;56.12B&lt;/td&gt;
&lt;td&gt;12.79B&lt;/td&gt;
&lt;td&gt;4.4×&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;branch-misses&lt;/td&gt;
&lt;td&gt;100.4M&lt;/td&gt;
&lt;td&gt;14.2M&lt;/td&gt;
&lt;td&gt;7.1×&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;L1 dcache misses&lt;/td&gt;
&lt;td&gt;663.8M&lt;/td&gt;
&lt;td&gt;159.0M&lt;/td&gt;
&lt;td&gt;4.2×&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p&gt;热点符号首位是 &lt;code&gt;pthread_mutex_lock&lt;/code&gt;（9.83% / 9.01%），占绝对优势。系统时间上升 9.6 倍，热点为互斥量，且该区间内确实改动过调度器的分发逻辑。基于这些证据得出的初步结论是：回归源于调度器分发路径上的锁竞争。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;该结论是错误的。&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;纠正它的方法不是更细粒度的 profile，而是正交回退：建立两个独立的工作树，分别只回退一侧的改动。&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;只回退 benchmark&lt;/td&gt;
&lt;td&gt;调度器改动&lt;/td&gt;
&lt;td&gt;3 个 benchmark 文件&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;8,037,018 / 7,876,556 ops/s —— 恢复&lt;/strong&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;只回退调度器&lt;/td&gt;
&lt;td&gt;benchmark 改动&lt;/td&gt;
&lt;td&gt;调度器头文件与实现&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;844,806 / 757,841 ops/s —— 仍然慢&lt;/strong&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p&gt;实际原因是 benchmark 热循环中的一次整数取模：生成键的函数无条件计算 &lt;code&gt;op_index % client_keys.size()&lt;/code&gt;，即每次迭代一次整数除法。在每秒数百万次的循环中，它把系统时间推高了 9.6 倍，并使 &lt;code&gt;pthread_mutex_lock&lt;/code&gt; 升至火焰图首位。&lt;/p&gt;
&lt;p&gt;这一案例的方法论含义是明确的：&lt;strong&gt;profiler 报告的是等待发生在何处，而非等待由谁导致&lt;/strong&gt;。&lt;code&gt;pthread_mutex_lock&lt;/code&gt; 位居首位只说明存在线程在锁上等待，不说明这些线程为何同时到达。当热点符号恰好与近期改动重合时，相关性会被误读为因果性。建立因果关系需要能够独立操纵变量的实验，正交回退是其中最直接的一种。&lt;/p&gt;
&lt;h2&gt;6. 判定性实验：确认瓶颈不在调度器&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;ops/s&lt;/th&gt;
&lt;th&gt;p50&lt;/th&gt;
&lt;th&gt;p95&lt;/th&gt;
&lt;th&gt;CPU 利用&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;478,149&lt;/td&gt;
&lt;td&gt;1.86&lt;/td&gt;
&lt;td&gt;2.93&lt;/td&gt;
&lt;td&gt;1.6 CPUs&lt;/td&gt;
&lt;td&gt;mutex 5.1M&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;稳态读密集&lt;/td&gt;
&lt;td&gt;967,225&lt;/td&gt;
&lt;td&gt;2.45&lt;/td&gt;
&lt;td&gt;10.31&lt;/td&gt;
&lt;td&gt;1.9 CPUs&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;rwlock 32.5M&lt;/strong&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;executor 压力&lt;/td&gt;
&lt;td&gt;667,590&lt;/td&gt;
&lt;td&gt;8.20&lt;/td&gt;
&lt;td&gt;25.78&lt;/td&gt;
&lt;td&gt;2.0 CPUs&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;rwlock 75.6M&lt;/strong&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;compaction 重叠&lt;/td&gt;
&lt;td&gt;489,597&lt;/td&gt;
&lt;td&gt;5.52&lt;/td&gt;
&lt;td&gt;14.37&lt;/td&gt;
&lt;td&gt;2.1 CPUs&lt;/td&gt;
&lt;td&gt;mutex 22.8M&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p&gt;关键论证：当客户端数翻倍、worker 数减半时，rwlock 竞争放大 &lt;strong&gt;2.3 倍（32.5M → 75.6M）&lt;/strong&gt;，表明 &lt;code&gt;shared_mutex&lt;/code&gt; 是串行化点。同时所有场景的 CPU 利用率均只有 1.6–2.1 个核，说明线程处于阻塞状态而非计算状态。&lt;/p&gt;
&lt;p&gt;据此形成两个判定：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;执行器重设计：否决&lt;/strong&gt;——队列与锁开销不是主要 CPU 消耗，平均延迟低于 10 µs&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;读视图重设计：通过&lt;/strong&gt;——读路径的引用计数操作可测量（32.5M–75.6M 锁采样）且影响实质（4c/4w 到 8c/2w 损失 31% 吞吐）&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;后续实施的发布式读视图（SuperVersion）结果：&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;delta&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;ops/s&lt;/td&gt;
&lt;td&gt;967,225&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;1,770,978&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;+83.1%&lt;/strong&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;p50&lt;/td&gt;
&lt;td&gt;2.45 µs&lt;/td&gt;
&lt;td&gt;1.88 µs&lt;/td&gt;
&lt;td&gt;−23.3%&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;p95&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;10.31 µs&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;4.19 µs&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;−59.4%&lt;/strong&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p&gt;写路径小幅回退 7.3%，原因是写路径仍使用独占锁，且 8 clients / 2 workers 的配置受限于执行器而非锁。&lt;/p&gt;
&lt;p&gt;同一轮中还有一个量化后决定推迟的候选项。针对 &lt;code&gt;std::function&lt;/code&gt; 的替换测得：其开销约占单操作 CPU 时间的 7%（约 1.03 µs 中的 73 ns），增量堆分配占约 3%（31 ns），而 TMA 显示 60.2% Backend Bound——真正的限制是内存延迟而非可调用对象分派。结论为推迟，并同时写死了复查触发条件：若 work lambda 的捕获集缩小至 SBO 可容纳、或负载转为 CPU-bound、或因 move-only 捕获本就需要自定义可调用类型，则重新评估。&lt;/p&gt;
&lt;p&gt;后续引入 &lt;code&gt;InlineJob&lt;/code&gt; 正是因为第三个条件成立（迁移至 &lt;code&gt;std::move_only_function&lt;/code&gt; 后捕获集缩小），而非基于主观判断。&lt;/p&gt;
&lt;h2&gt;7. 移除抽象&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;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;IScheduler&lt;/code&gt; 虚接口&lt;/td&gt;
&lt;td&gt;每次提交一次虚分派，其提供的可替换性从未被使用&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;ExecutorSchedulerAdapter&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;只把调用转发给另一个执行器的壳层&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;IContinuationExecutor&lt;/code&gt; / &lt;code&gt;ThreadPoolExecutor&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;同上&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;BlockingScheduler()&lt;/code&gt; / &lt;code&gt;ContinuationScheduler()&lt;/code&gt; 路由钩子&lt;/td&gt;
&lt;td&gt;绝大多数实现返回 &lt;code&gt;this&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;pending_list_&lt;/code&gt; / &lt;code&gt;pending_mutex_&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;快路径落地后，前台任务不再经过空闲登记表&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;AsyncEnvBackendMode&lt;/code&gt; 枚举&lt;/td&gt;
&lt;td&gt;三个枚举值解析到同一个执行器&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;runtime 全局注册表与延迟销毁队列&lt;/td&gt;
&lt;td&gt;改为显式构造、显式持有&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;AsyncOp&lt;/code&gt; 的虚 &lt;code&gt;StateBase::Execute()&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;在热路径上增加了一层 vtable 分派，无对应收益&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p&gt;类型名同时由 &lt;code&gt;ThreadPoolScheduler&lt;/code&gt; 改为 &lt;code&gt;CpuThreadPool&lt;/code&gt;。这不是修辞性的改动：该类的实际职责是 CPU 执行池，而非通用调度器，旧名称持续误导了对其定位的判断。&lt;/p&gt;
&lt;p&gt;&lt;code&gt;ContinuationScheduler()&lt;/code&gt; 的移除具有代表性。它在代码中有十余处引用，表面上支撑着一条重要的路由分支，但生产路径上从未被调用——&lt;code&gt;AsyncOp&lt;/code&gt; 采用内联恢复。更严重的是，头文件注释明确描述了 AsyncOp 如何通过它恢复协程。&lt;strong&gt;一个从未被执行的分支，配以一段描述其执行方式的注释&lt;/strong&gt;，比缺陷更难被发现：它不会产生错误行为，只会使后续设计建立在一个不存在的机制之上。&lt;/p&gt;
&lt;p&gt;由此形成的架构准则是：&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;应当只有一条前台调度路径，加上少数几条物理隔离 lane。不应新增逻辑调度器，除非它保护顺序/正确性，或隔离了真实的阻塞工作。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;判定一条 lane 是否应当存在的标准只有两条：它是否保护顺序或正确性；它是否隔离了真实的阻塞工作。二者皆否时，它只是一层不提供任何隔离的转发壳。&lt;/p&gt;
&lt;hr /&gt;
&lt;h1&gt;第二部分 · 当前架构&lt;/h1&gt;
&lt;p&gt;以下内容以当前提交为准。它描述的是一组彼此约束的执行域，而不是一个孤立的线程池。&lt;/p&gt;
&lt;h2&gt;8. 设计目标与非目标&lt;/h2&gt;
&lt;p&gt;&lt;strong&gt;目标&lt;/strong&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;li&gt;协程恢复默认不产生额外的队列跳转&lt;/li&gt;
&lt;li&gt;关机时不丢失任何已提交的工作&lt;/li&gt;
&lt;li&gt;任何未捕获异常都是缺陷，采取失败快速策略&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;&lt;strong&gt;非目标&lt;/strong&gt;&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;不保证跨 worker 的全局提交顺序&lt;/li&gt;
&lt;li&gt;不提供抢占；所有任务运行至完成&lt;/li&gt;
&lt;li&gt;不提供抢占式优先级或端到端公平性界限；优先级只规定中央回落队列的取出顺序&lt;/li&gt;
&lt;li&gt;不提供跨调度器实例的工作迁移&lt;/li&gt;
&lt;li&gt;不追求通用性——每一条 lane 都是为具体的调用点设立的&lt;/li&gt;
&lt;/ol&gt;
&lt;h2&gt;9. 分层结构&lt;/h2&gt;
&lt;pre&gt;&lt;code&gt;graph TD
    subgraph L4[&quot;Layer 4 — 公共 API&quot;]
        ADB[&quot;AsyncDB&quot;]
        AEnv[&quot;AsyncEnv&quot;]
        AF[&quot;AsyncFile&quot;]
    end
    subgraph L3[&quot;Layer 3 — Awaitable 桥接&quot;]
        AOp[&quot;AsyncOp&amp;amp;lt;T&amp;amp;gt;&quot;]
        AWOp[&quot;AsyncWriteOp&quot;]
    end
    subgraph L2[&quot;Layer 2 — 异步运行时（六条物理 lane）&quot;]
        CPU[&quot;cpu_executor&quot;]
        DBR[&quot;db_read_executor&quot;]
        DBW[&quot;db_write_executor&quot;]
        BIO[&quot;blocking_io_executor&quot;]
        COMP[&quot;compaction_executor&quot;]
        SER[&quot;serial_file_executor&quot;]
    end
    subgraph L1[&quot;并发控制支持&quot;]
        STOP[&quot;StopSource / StopToken&quot;]
        SCOPE[&quot;TaskScope&quot;]
        Q[&quot;Quarantine&quot;]
    end
    ADB --&amp;gt; AOp
    ADB --&amp;gt; AWOp
    AEnv --&amp;gt; AOp
    AF --&amp;gt; AOp
    AOp --&amp;gt; DBR
    AOp --&amp;gt; BIO
    AOp --&amp;gt; COMP
    AOp --&amp;gt; SER
    AWOp --&amp;gt; DBW
    STOP --&amp;gt; COMP
    SCOPE --&amp;gt; STOP
    SCOPE --&amp;gt; Q
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;层间的核心约束是：&lt;strong&gt;上层只能&quot;提交到某个执行器&quot;，不能&quot;查询应当提交到哪个执行器&quot;&lt;/strong&gt;。路由是构造期确定的静态事实，不是运行期的动态决策。移除 &lt;code&gt;IScheduler&lt;/code&gt; 系列抽象换来的正是这一约束——它把&quot;目标执行器&quot;从一个可以在运行时改变的间接层，变成了调用点上可见的常量。&lt;/p&gt;
&lt;h3&gt;9.1 所有权与构造边界&lt;/h3&gt;
&lt;p&gt;&lt;code&gt;AsyncRuntime&lt;/code&gt; 并不拥有 CPU 池，而是保存对外部 &lt;code&gt;CpuThreadPool&lt;/code&gt; 的引用。其余五条 lane 与 &lt;code&gt;IoDispatcher&lt;/code&gt; 由运行时按值持有：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;class AsyncRuntime {
    CpuThreadPool&amp;amp; cpu_executor_;                 // 非拥有
    BlockingExecutor db_read_executor_;           // 拥有
    SerialExecutor db_write_executor_;            // 拥有
    BlockingExecutor blocking_io_executor_;       // 拥有
    IoDispatcher io_dispatcher_;                  // 借用 blocking_io_executor_
    BlockingExecutor compaction_executor_;        // 拥有
    SerialExecutor serial_file_executor_;         // 拥有
};
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;成员声明顺序同时规定了析构顺序。C++ 按声明的逆序析构成员，因此 &lt;code&gt;io_dispatcher_&lt;/code&gt; 会先于它所借用的 &lt;code&gt;blocking_io_executor_&lt;/code&gt; 停止；CPU 池则必须由运行时的外部所有者保证存活。这里的所有权关系并非实现细节，因为 &lt;code&gt;AsyncOp&lt;/code&gt; 和 I/O 完成回调均保存非拥有的执行器引用。&lt;/p&gt;
&lt;p&gt;数据库实例在打开时进一步建立两条面向存储语义的绑定：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;DBImpl
├── WriteCoordinator  ──借用──&amp;gt; db_write_executor
└── CompactionController ─借用──&amp;gt; compaction_executor
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;因此，写入顺序与 compaction 单飞不由调用者临时选择，而是在 &lt;code&gt;DBImpl&lt;/code&gt; 的构造阶段固定下来。&lt;/p&gt;
&lt;h2&gt;10. 执行 lane 规格&lt;/h2&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Lane&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;&lt;strong&gt;CPU 池&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;CpuThreadPool&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;max(hw, 2)&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;短续体、定时任务、reactor 读的协程恢复&lt;/td&gt;
&lt;td&gt;reactor 完成回调将在 pump 线程上执行用户逻辑，阻塞后续完成事件的收割&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;DB 读&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;BlockingExecutor&lt;/code&gt;，开启 stealing&lt;/td&gt;
&lt;td&gt;4&lt;/td&gt;
&lt;td&gt;&lt;code&gt;GetAsync&lt;/code&gt; 内部为同步 &lt;code&gt;DBImpl::Get&lt;/code&gt;，会遭遇 cache miss 与元数据锁&lt;/td&gt;
&lt;td&gt;阻塞读占用 CPU 池 worker&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;DB 写&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;SerialExecutor&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;1&lt;/td&gt;
&lt;td&gt;写顺序即正确性&lt;/td&gt;
&lt;td&gt;写组顺序不可控；memtable 需要额外的多写者保护&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;阻塞 I/O&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;BlockingExecutor&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;4&lt;/td&gt;
&lt;td&gt;&lt;code&gt;Env&lt;/code&gt; 元数据、reactor 无法处理的文件读、文件打开&lt;/td&gt;
&lt;td&gt;一次 &lt;code&gt;stat&lt;/code&gt; 可以卡住协程续体&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Compaction&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;BlockingExecutor&lt;/code&gt;，&lt;strong&gt;关闭 stealing&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;1&lt;/td&gt;
&lt;td&gt;后台、阻塞、单飞&lt;/td&gt;
&lt;td&gt;毫秒级 compaction 排在微秒级点读之前&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;code&gt;SerialExecutor&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;1&lt;/td&gt;
&lt;td&gt;append/flush/sync/close 的 FIFO 顺序&lt;/td&gt;
&lt;td&gt;文件操作乱序完成&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;pre&gt;&lt;code&gt;graph LR
    subgraph Work[&quot;工作来源&quot;]
        COMPACTION[&quot;Compaction&quot;]
        IO[&quot;文件 I/O&amp;lt;br/&amp;gt;(AsyncEnv)&quot;]
        DBOPEN[&quot;DB Open&quot;]
        DBGET[&quot;DB Get&quot;]
        DBWRITE[&quot;DB Put/Delete/Write&quot;]
        WRITE[&quot;有序文件写&quot;]
    end
    subgraph Execs[&quot;Executors&quot;]
        READ[&quot;blocking_io_executor (4)&quot;]
        DBREAD[&quot;db_read_executor (4)&quot;]
        DBWR[&quot;db_write_executor (1)&quot;]
        COMP[&quot;compaction_executor (1)&quot;]
        CPU[&quot;cpu_executor (N)&quot;]
        SER[&quot;serial_file_executor (1)&quot;]
    end
    COMPACTION --&amp;gt; COMP
    IO --&amp;gt; READ
    DBOPEN --&amp;gt; READ
    DBGET --&amp;gt; DBREAD
    DBWRITE --&amp;gt; DBWR
    WRITE --&amp;gt; SER
    DBREAD --&amp;gt;|&quot;内联恢复协程&quot;| DBREAD
    DBWR --&amp;gt;|&quot;AsyncWriteOp 恢复&quot;| DBWR
    READ --&amp;gt;|&quot;内联恢复协程&quot;| READ
    SER --&amp;gt;|&quot;内联恢复协程&quot;| SER
    READ --&amp;gt;|&quot;reactor 读完成，跨线程恢复&quot;| CPU
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;图中最后一条边表示文件读取的恢复边界：无论 I/O 由 reactor 还是阻塞回落路径完成，&lt;code&gt;async_env.cpp&lt;/code&gt; 都会把续体提交到 CPU 池。一般的 &lt;code&gt;AsyncOp&lt;/code&gt; 则在完成工作的 executor 线程上内联恢复，其依据见第 15 节。&lt;/p&gt;
&lt;p&gt;线程预算方面，默认配置包括：外部 CPU 池的 N 个 worker 与两条分发线程，DB 读 4、阻塞 I/O 4、compaction 1、DB 写 1、有序文件 1，以及 reactor 的一条 pump 线程。除 CPU 池外，各 lane 的线程数在构造时确定，运行期间不自动伸缩。工作窃取只能在同一个 &lt;code&gt;BlockingExecutor&lt;/code&gt; 实例的分片之间发生，不能跨越 lane；物理隔离因而不会被负载均衡机制破坏。&lt;/p&gt;
&lt;h2&gt;11. 作业表示：&lt;code&gt;InlineJob&lt;/code&gt;&lt;/h2&gt;
&lt;pre&gt;&lt;code&gt;class InlineJob
{
public:
    static constexpr std::size_t kInlineBytes = 128;

private:
    using InvokeFn  = void (*)(void*);
    using DestroyFn = void (*)(void*) noexcept;
    using DeleteFn  = void (*)(void*) noexcept;
    using MoveFn    = void (*)(void*, void*) noexcept;

    template &amp;lt;typename Fn&amp;gt;
    static constexpr bool kUseInlineStorage = sizeof(Fn) &amp;lt;= kInlineBytes
        &amp;amp;&amp;amp; alignof(Fn) &amp;lt;= alignof(std::max_align_t)
        &amp;amp;&amp;amp; std::is_nothrow_move_constructible_v&amp;lt;Fn&amp;gt;;

    alignas(std::max_align_t) std::byte storage_[kInlineBytes];
    void* ptr_{ nullptr };
    InvokeFn invoke_{ nullptr };
    DestroyFn destroy_{ nullptr };
    DeleteFn delete_{ nullptr };
    MoveFn move_{ nullptr };
    bool heap_allocated_{ false };
};
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;这是一个 move-only 的 &lt;code&gt;void()&lt;/code&gt; 包装，以四个函数指针手工实现类型擦除，而非依赖编译器生成的 vtable。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;存储策略&lt;/strong&gt;。满足三个条件时使用内联存储，否则堆分配：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;if constexpr (kUseInlineStorage&amp;lt;Fn&amp;gt;) {
    ptr_ = StoragePtr();
    std::construct_at(static_cast&amp;lt;Fn*&amp;gt;(ptr_), std::forward&amp;lt;F&amp;gt;(f));
    heap_allocated_ = false;
} else {
    ptr_ = new Fn(std::forward&amp;lt;F&amp;gt;(f));
    heap_allocated_ = true;
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;三个条件各自必要。尺寸约束是显然的；对齐约束保证 &lt;code&gt;storage_&lt;/code&gt; 的 &lt;code&gt;max_align_t&lt;/code&gt; 对齐足够；&lt;code&gt;is_nothrow_move_constructible_v&lt;/code&gt; 是关键的一条——移动 &lt;code&gt;InlineJob&lt;/code&gt; 时需要把内联存储中的对象搬到新位置，该步骤不能抛出，否则移动构造函数无法保持 &lt;code&gt;noexcept&lt;/code&gt;，而容器操作依赖这一点。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;移动语义的分支&lt;/strong&gt;。堆分配的情形只需转移指针；内联的情形必须逐对象移动构造，并显式析构源对象：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;if (other.heap_allocated_) {
    ptr_ = other.ptr_;
    other.ptr_ = nullptr;
} else {
    ptr_ = StoragePtr();
    move_(other.ptr_, ptr_);
    other.destroy_(other.ptr_);
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;strong&gt;空间与分配的交换&lt;/strong&gt;。&lt;code&gt;sizeof(InlineJob)&lt;/code&gt; 为 176 字节。即每个队列槽位以 176 字节的常驻空间，换取绝大多数作业零堆分配。该交换的有效性取决于队列深度：在实测平均深度 1.3、作业生命周期为微秒级的场景下是有利的；若队列会堆积上万个作业，该结论应当反转。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;与 &lt;code&gt;std::move_only_function&lt;/code&gt; 的关系&lt;/strong&gt;。后者曾是 &lt;code&gt;Job&lt;/code&gt; 的实现，替换的动机是其小对象优化容量不足以容纳典型的 work lambda（捕获集包含 &lt;code&gt;shared_ptr&lt;/code&gt;、&lt;code&gt;string&lt;/code&gt; 与 options，约 64 字节，超出 libstdc++ 24 字节的可用 SBO），从而每次提交触发一次堆分配。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;配套的就地构造&lt;/strong&gt;。以 C++20 &lt;code&gt;requires&lt;/code&gt; 约束的模板重载，使调用点直接把可调用对象构造进目标队列槽位，省去一次 &lt;code&gt;InlineJob&lt;/code&gt; 移动：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;template &amp;lt;typename F&amp;gt;
    requires(!std::is_same_v&amp;lt;std::decay_t&amp;lt;F&amp;gt;, Job&amp;gt;)
void Submit(F&amp;amp;&amp;amp; job) { SubmitJob(std::forward&amp;lt;F&amp;gt;(job), 0); }
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;同样的模式贯穿 &lt;code&gt;SubmitAfter&lt;/code&gt;、&lt;code&gt;SubmitIn&lt;/code&gt;、&lt;code&gt;WorkThread::Emplace&lt;/code&gt; 与 &lt;code&gt;PushToPriorityQueue&lt;/code&gt;。这也是 &lt;code&gt;thread_local&lt;/code&gt; 状态由匿名命名空间提升为类静态成员的原因——模板实现必须放在头文件中。&lt;/p&gt;
&lt;h2&gt;12. 提交引用：&lt;code&gt;ExecutorRef&lt;/code&gt;&lt;/h2&gt;
&lt;pre&gt;&lt;code&gt;struct ExecutorRef
{
    using Job = InlineJob;

    void* executor = nullptr;
    void (*submit)(void*, Job) = nullptr;

    ExecutorRef() = default;

    template &amp;lt;typename Executor&amp;gt;
        requires(!std::is_same_v&amp;lt;std::decay_t&amp;lt;Executor&amp;gt;, ExecutorRef&amp;gt;)
    explicit ExecutorRef(Executor&amp;amp; target)
        : executor(&amp;amp;target)
        , submit([](void* ptr, Job job) { static_cast&amp;lt;Executor*&amp;gt;(ptr)-&amp;gt;Submit(std::move(job)); })
    {}

    void Submit(Job job) const { submit(executor, std::move(job)); }
};
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;一个非拥有的、非虚的提交引用。它取代了 &lt;code&gt;IScheduler&lt;/code&gt; 虚接口，语义上只提供一个操作：把作业提交到构造时绑定的执行器。&lt;/p&gt;
&lt;p&gt;它&lt;strong&gt;不提供&lt;/strong&gt;的操作同样重要：不能查询目标执行器的身份，不能改变路由，不能获取&quot;阻塞执行器&quot;或&quot;续体执行器&quot;。这些能力在旧设计中存在，且正是它们使得&quot;目标执行器&quot;成为一个运行时可变的间接层，进而使调用点无法在静态上确定工作会在哪里执行。&lt;/p&gt;
&lt;p&gt;调度器头文件与运行时头文件中 &lt;code&gt;virtual&lt;/code&gt; 关键字出现零次。&lt;/p&gt;
&lt;h2&gt;13. CPU 执行池：&lt;code&gt;CpuThreadPool&lt;/code&gt;&lt;/h2&gt;
&lt;h3&gt;13.1 内部结构&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;CpuThreadPool
├── work_threads_[N]            每 worker：mutex + deque&amp;lt;QueuedJob&amp;gt; + counting_semaphore + atomic load
├── submit_cursor_              外部提交的轮转游标（relaxed fetch_add）
├── priority_queue_ + mutex     非零优先级 / 快路径回落
├── lazy_queue_ + mutex         延时任务（按 deadline 的最小堆）
├── priority_waiter_            优先级分发线程的信号量
├── lazy_waiter_                定时分发线程的信号量
├── priority_thread_            优先级分发线程
├── lazy_thread_                定时分发线程
├── exit_flag_                  关机标志
└── thread_local current_scheduler_ / current_worker_index_
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;code&gt;thread_local&lt;/code&gt; 的两个变量承担了两项职责。其一是快路径判定：把&quot;当前线程是否属于本池&quot;退化为一次指针比较。其二是为 &lt;code&gt;Context&lt;/code&gt; 提供实例身份——从其他池捕获的 context 会被识别并安全回落，而不是索引到本池中同序号的 worker。&lt;/p&gt;
&lt;h3&gt;13.2 四条提交路径&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;code&gt;Submit(job)&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;自提交回本 worker；外部按 &lt;code&gt;submit_cursor_&lt;/code&gt; 轮转&lt;/td&gt;
&lt;td&gt;是&lt;/td&gt;
&lt;td&gt;reactor 读的协程恢复&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;SubmitWithPriority(job, p)&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;p == 0&lt;/code&gt; 同上；&lt;code&gt;p &amp;gt; 0&lt;/code&gt; 进优先队列并由分发线程投递&lt;/td&gt;
&lt;td&gt;是&lt;/td&gt;
&lt;td&gt;无&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;SubmitAfter(deadline, job)&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;进 &lt;code&gt;lazy_queue_&lt;/code&gt;，到期后直推负载最低的 worker，失败回落优先队列&lt;/td&gt;
&lt;td&gt;是&lt;/td&gt;
&lt;td&gt;无&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;SubmitIn(ctx, job)&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;严格投递到 &lt;code&gt;ctx&lt;/code&gt; 指定的 worker；context 无效或跨池则回落 &lt;code&gt;Submit&lt;/code&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;code&gt;ChooseSubmissionWorker()&lt;/code&gt; 是一次 relaxed 的 &lt;code&gt;fetch_add&lt;/code&gt; 取模，用于外部提交的快路径，代价常数；&lt;code&gt;ChooseLeastLoadedWorker()&lt;/code&gt; 遍历所有 worker 的近似负载计数，代价 O(N)，只用于延时任务投递与关机排空——这些路径频率低，可以承受扫描。&lt;/p&gt;
&lt;p&gt;&lt;code&gt;SubmitIn&lt;/code&gt; 的契约有一点反直觉，因而在头文件中写了完整说明：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;// Strict affinity: when ctx was captured from a worker of THIS scheduler instance,
// the job is pushed directly to that exact worker&apos;s local queue. The job remains
// stealable — other workers may steal it under load, but the affinity-push
// maximizes the chance the same-worker executes it.
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;strong&gt;亲和是尽力而为，而非保证。&lt;/strong&gt; 若保证亲和，则单个 worker 阻塞会拖住绑定其上的全部任务；允许被偷取则以缓存局部性换取活性。该设计选择了活性。&lt;/p&gt;
&lt;h3&gt;13.3 消费循环&lt;/h3&gt;
&lt;p&gt;worker 的消费循环是两层结构：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;while (true) {
    semaphore_.acquire();                          // 外层：无工作时完全阻塞
    while (true) {
        // 1. 从本地队列出队并执行
        // 2. 本地为空 → TrySteal → 成功则 continue
        // 3. 偷不到且已请求退出 → break
        if (!semaphore_.try_acquire_for(kStealBackoff)) break;   // 退回外层重新阻塞
    }
    if (scheduler.IsExitRequested()) {
        std::lock_guard lock(mutex_);
        if (queue_.empty()) break;
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;外层的无限期阻塞保证空闲 worker 不消耗 CPU；内层的 100 µs 定时等待使得刚刚变空的 worker 在短时间内保持&quot;热&quot;，避免频繁的完全阻塞与唤醒。&lt;code&gt;kStealBackoff&lt;/code&gt; 的作用是把&quot;偷取失败&quot;与&quot;进入深度休眠&quot;分开：偷取失败不立即休眠，而是给上游一个短窗口继续投递。&lt;/p&gt;
&lt;p&gt;关机时的退出条件是合取式的——&lt;strong&gt;必须同时满足&quot;已请求退出&quot;与&quot;本地队列为空&quot;&lt;/strong&gt;。只检查退出标志会导致快路径任务在关机时被遗留在 worker 队列中。&lt;/p&gt;
&lt;h3&gt;13.4 策略与机制的分层&lt;/h3&gt;
&lt;p&gt;头文件中显式区分两类辅助函数：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;策略&lt;/strong&gt;（路径选择判定）：&lt;code&gt;ShouldUseFastPath(priority)&lt;/code&gt;、&lt;code&gt;ShouldAcceptSubmitDuringShutdown()&lt;/code&gt;、&lt;code&gt;ShouldPromoteLazyTask(task, now)&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;机制&lt;/strong&gt;（队列与线程操作）：&lt;code&gt;PushToPriorityQueue(job, priority, wake)&lt;/code&gt;、&lt;code&gt;TryPushToWorker&lt;/code&gt; / &lt;code&gt;TryEmplaceToWorker&lt;/code&gt;、&lt;code&gt;DispatchExpiredTask&lt;/code&gt;、&lt;code&gt;PromoteLazyResidueToWorkers&lt;/code&gt;、&lt;code&gt;DrainLazyQueueToEmpty&lt;/code&gt;、&lt;code&gt;DrainPriorityQueueToEmpty&lt;/code&gt;、&lt;code&gt;DrainWorkerLocalQueues&lt;/code&gt;、&lt;code&gt;TryDequeueJob&lt;/code&gt;、&lt;code&gt;HandleJobCompletion&lt;/code&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;这一划分的实际收益是：策略函数全部是纯判定，可以独立阅读与测试；机制函数全部无分支决策，可以独立验证其效果。析构函数的排空主体因此从约 45 行内联代码缩减为 4 次具名调用，消费循环的作业处理从约 28 行缩减为 5 行。&lt;/p&gt;
&lt;h2&gt;14. 三类执行器与工作窃取&lt;/h2&gt;
&lt;p&gt;当前运行时使用三种执行器。它们共享 &lt;code&gt;InlineJob&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;CpuThreadPool&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;每 worker 一个 deque，另有优先级与延时队列&lt;/td&gt;
&lt;td&gt;普通提交无全局顺序；支持尽力而为的亲和&lt;/td&gt;
&lt;td&gt;短续体、延时任务&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;BlockingExecutor&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;每 worker 一个 64 字节对齐的 shard&lt;/td&gt;
&lt;td&gt;多线程时无全局 FIFO；可按实例关闭 stealing&lt;/td&gt;
&lt;td&gt;DB 读、阻塞 I/O、compaction&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;SerialExecutor&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;单 deque、单 worker、condition variable&lt;/td&gt;
&lt;td&gt;提交顺序上的 FIFO&lt;/td&gt;
&lt;td&gt;DB 写协调、有序文件操作&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p&gt;&lt;code&gt;SerialExecutor&lt;/code&gt; 的价值不在于减少锁开销，而在于将“同一时刻至多有一个工作推进”编码为执行器性质。&lt;code&gt;BlockingExecutor&lt;/code&gt; 则把阻塞线程限制在所属 lane 内。&lt;code&gt;CpuThreadPool&lt;/code&gt; 保留优先级、延时与亲和接口，但其普通前台提交路径直接面向 worker-local 队列。&lt;/p&gt;
&lt;h3&gt;14.1 &lt;code&gt;CpuThreadPool&lt;/code&gt; 的窃取协议&lt;/h3&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;维度&lt;/th&gt;
&lt;th&gt;策略&lt;/th&gt;
&lt;th&gt;依据&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;触发&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;xorshift64 随机，均匀排除自身&lt;/td&gt;
&lt;td&gt;避免扫描全部 worker 重新引入竞争&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;加锁&lt;/td&gt;
&lt;td&gt;&lt;code&gt;scoped_lock&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;加锁期间可能已有新任务投递到自身&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;偷取量&lt;/td&gt;
&lt;td&gt;可偷任务的一半，至少 1 个&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;code&gt;try_acquire_for(100µs)&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;定时重试允许，忙等禁止&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p&gt;负载计数器 &lt;code&gt;load_&lt;/code&gt; 以 &lt;code&gt;memory_order_relaxed&lt;/code&gt; 维护，是&lt;strong&gt;近似值&lt;/strong&gt;。这是有意的：它只用于选择投递目标，误选的代价是一次轻微的负载不均，而 stealing 会纠正它；为此付出顺序一致性的代价不划算。&lt;/p&gt;
&lt;h3&gt;14.2 &lt;code&gt;BlockingExecutor&lt;/code&gt; 的分片&lt;/h3&gt;
&lt;p&gt;&lt;code&gt;BlockingExecutor&lt;/code&gt; 采用相同的“本地优先、空闲时窃取”原则，但队列被组织为独立分片：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;struct alignas(64) Shard {          // 64 字节对齐，隔离伪共享
    std::mutex mutex;
    std::deque&amp;lt;Job&amp;gt; queue;
    std::counting_semaphore&amp;lt;&amp;gt; semaphore{ 0 };
    std::atomic&amp;lt;std::size_t&amp;gt; load{ 0 };
};
std::vector&amp;lt;std::unique_ptr&amp;lt;Shard&amp;gt;&amp;gt; shards_;
std::atomic&amp;lt;std::size_t&amp;gt; submit_cursor_{ 0 };
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;分片以 &lt;code&gt;unique_ptr&lt;/code&gt; 持有而非直接放入 &lt;code&gt;vector&lt;/code&gt;，以保证 &lt;code&gt;alignas(64)&lt;/code&gt; 在动态分配下仍然生效。&lt;/p&gt;
&lt;p&gt;&lt;code&gt;enable_stealing&lt;/code&gt; 是构造参数。compaction lane 以单 worker 构造并显式关闭 stealing。需要注意，stealing 本来就不会跨 &lt;code&gt;BlockingExecutor&lt;/code&gt; 实例发生；关闭该选项表达的是 compaction lane 自身不需要并行窃取语义，而不是防止它从 DB 读 lane 获取任务。&lt;/p&gt;
&lt;p&gt;&lt;code&gt;BlockingExecutor&lt;/code&gt; 另有一个重入快路径：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;template &amp;lt;typename F&amp;gt;
bool TryRunInline(F&amp;amp;&amp;amp; work)
{
    if (!IsCurrentWorker()) return false;
    work();
    return true;
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;返回 &lt;code&gt;bool&lt;/code&gt; 而非直接排队，使调用方可以在内联检查失败之后再惰性构造排队所需的闭包——避免为一条大概率不会走的路径提前付出构造代价。&lt;/p&gt;
&lt;p&gt;&lt;code&gt;AsyncGetOp::State::Start&lt;/code&gt; 是该接口的实际调用点。若一次点读在 DB 读 worker 上递归发起，工作会直接执行；否则才构造闭包并提交到 &lt;code&gt;db_read_executor_&lt;/code&gt;。这一安排避免了同一 lane 内部不必要的再次排队，同时没有把该优化开放为跨 lane 的内联执行。&lt;/p&gt;
&lt;h2&gt;15. 协程恢复：三态握手&lt;/h2&gt;
&lt;p&gt;&lt;code&gt;AsyncOp&lt;/code&gt; 的类注释完整描述了它要关闭的竞态：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;// Suspend/Resume Handshake (race safety):
// There is a classic race between the worker completing and the coroutine suspending:
//
//   Thread A (coroutine): enters await_suspend(), submits work, then sets state to kSuspended.
//   Thread B (worker):    completes work, then attempts to resume the coroutine.
//
// If Thread B runs to completion BEFORE Thread A transitions state to kSuspended,
// calling handle.resume() before the coroutine is actually suspended is UB.
//
// Protocol (atomic state machine, three states):
//   kSuspending → either kCompleted (worker wins) or kSuspended (coroutine wins)
//
// This guarantees:
//   - Exactly-once resume.
//   - No resume-before-suspend.
//   - No lost wakeup.
&lt;/code&gt;&lt;/pre&gt;
&lt;pre&gt;&lt;code&gt;stateDiagram-v2
    [*] --&amp;gt; kSuspending: co_await 开始
    kSuspending --&amp;gt; kSuspended: 协程胜出（await_suspend 返回 true）
    kSuspending --&amp;gt; kCompleted: worker 胜出（await_suspend 返回 false）
    kSuspended --&amp;gt; [*]: worker 恢复协程
    kCompleted --&amp;gt; [*]: 协程立即继续
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;竞态的两端通过一次 CAS 决出胜负：胜方负责推进，败方不做任何事。若协程胜出（先完成挂起），worker 后续调用 &lt;code&gt;handle.resume()&lt;/code&gt;；若 worker 胜出（工作先完成），&lt;code&gt;await_suspend&lt;/code&gt; 返回 &lt;code&gt;false&lt;/code&gt;，协程根本不挂起，直接继续执行。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;恢复位置&lt;/strong&gt;。默认情况下，恢复内联在完成该工作的 worker 上，不再向队列投递一次。这消除了每个操作一次额外的 &lt;code&gt;Submit()&lt;/code&gt;——在每秒数百万次操作的规模下，这一跳的成本可观测。&lt;/p&gt;
&lt;p&gt;代价是完成线程会继续执行用户的协程体。对阻塞 lane 而言这是正确的（它本就用于执行阻塞工作）；对 reactor pump 线程而言这是错误的，因此那条路径是唯一的例外：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;reactor 完成 → 存储结果 → CpuExecutor().Submit(resume)
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;pump 线程只有一条，在其上执行任意用户逻辑会阻塞后续所有完成事件的收割。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;生命周期前提&lt;/strong&gt;。awaiter 的状态由挂起的协程帧持有（&lt;code&gt;unique_ptr&lt;/code&gt;），worker 在调用 &lt;code&gt;handle.resume()&lt;/code&gt; 之后不再接触该状态。因此不变量是：&lt;strong&gt;等待中的协程必须存活至恢复完成&lt;/strong&gt;。这一前提取代了早期版本中的 &lt;code&gt;shared_ptr&lt;/code&gt;，消除了每次操作一对原子的引用计数增减。&lt;/p&gt;
&lt;h2&gt;16. 异步 I/O：手写 io_uring reactor&lt;/h2&gt;
&lt;p&gt;&lt;code&gt;IoReactor&lt;/code&gt; 是直接针对 &lt;code&gt;linux/io_uring.h&lt;/code&gt; 的手写绑定，不依赖 liburing：raw &lt;code&gt;syscall(SYS_io_uring_setup / SYS_io_uring_enter / SYS_io_uring_register)&lt;/code&gt;，自行 &lt;code&gt;mmap&lt;/code&gt; 提交队列与完成队列环，自行填写 &lt;code&gt;io_uring_sqe&lt;/code&gt;，以 &lt;code&gt;std::atomic_ref&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;内核版本 ≥ 5.1，且一次 &lt;code&gt;io_uring_setup&lt;/code&gt; 成功（随后立即关闭）&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;环容量&lt;/td&gt;
&lt;td&gt;默认 256 entries&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;支持操作&lt;/td&gt;
&lt;td&gt;read / write / fsync&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;请求关联&lt;/td&gt;
&lt;td&gt;每请求分配 &lt;code&gt;user_data&lt;/code&gt;，pump 线程据此在 in-flight 表中查回调&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;能力级回落&lt;/td&gt;
&lt;td&gt;io_uring 不可用时，整条路径改走 &lt;code&gt;BlockingExecutor&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;操作级回落&lt;/td&gt;
&lt;td&gt;提交或完成结果表明操作不受支持时，执行对应的同步系统调用&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;恢复&lt;/td&gt;
&lt;td&gt;&lt;code&gt;IoDispatcher&lt;/code&gt; 调用完成回调；由上层决定内联推进还是向 CPU 池投递&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p&gt;&lt;code&gt;IoDispatcher&lt;/code&gt; 在其上提供请求队列、pump 线程与 in-flight 表。它负责完成事件的多路复用，但不直接规定协程应当在哪里恢复：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;调用方 → SubmitRead/Write/Fsync（附完成回调）
      → pending_requests_ 队列
      → pump 线程取出，填 sqe，io_uring_enter 提交
      → in_flight_[user_data] = state
      → pump 线程阻塞在 WaitCompletion
      → 完成事件到达 → 查表 → 执行完成回调
      → async_env 的读回调向 CPU 池提交续体
        或 AsyncWalWriter 继续提交 fsync / 完成写组
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;这种分层在实际代码中形成两条不同的完成链。&lt;/p&gt;
&lt;p&gt;对于随机读：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;IoDispatcher pump
  → 形成 Result&amp;lt;size_t&amp;gt;
  → CpuThreadPool::Submit
  → 运行 completion / 恢复读取协程
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;对于 WAL 写入：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;WriteCoordinator 规划写组
  → AsyncWalWriter::Write
  → io_uring write
  → 若 sync=true，再提交 io_uring fsync
  → 回调重新提交到 db_write_executor
  → CommitWriteGroupForCoordinator
  → 发布完成并恢复等待者
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;这表明 pump 线程并非纯粹的字节搬运线程：WAL 完成回调会在其上推进一个有限状态机，但真正的写组提交仍被重新送回串行写 lane。相反，文件随机读的用户续体始终经由 CPU 池恢复，以避免在唯一的 pump 线程上执行不可控的上层逻辑。&lt;/p&gt;
&lt;p&gt;当前实现还有一个明确边界：reactor 的单操作回落在 pump 循环中直接执行同步 &lt;code&gt;pread&lt;/code&gt;、&lt;code&gt;pwrite&lt;/code&gt; 或 &lt;code&gt;fsync&lt;/code&gt;。若内核在提交阶段或完成阶段拒绝某个 opcode，pump 线程可能因此阻塞。整条 reactor 不可用时的能力级回落则由 &lt;code&gt;BlockingExecutor&lt;/code&gt; 承担。二者不应混为同一种回落语义。&lt;/p&gt;
&lt;h2&gt;17. 结构化并发：取消与结果隔离&lt;/h2&gt;
&lt;p&gt;代码库还包含一组结构化并发原语，但其成熟度与生产接入程度并不相同。当前存储主路径实际使用的是 &lt;code&gt;StopSource&lt;/code&gt; / &lt;code&gt;StopToken&lt;/code&gt;：&lt;code&gt;CompactionController&lt;/code&gt; 以它们传递停止请求，&lt;code&gt;DBImpl::BackgroundCompaction&lt;/code&gt; 与 &lt;code&gt;DoCompactionWork&lt;/code&gt; 在阶段边界观察 token。&lt;code&gt;TaskScope&lt;/code&gt;、&lt;code&gt;OperationState&lt;/code&gt; 与 &lt;code&gt;Quarantine&lt;/code&gt; 已有实现和测试，但尚未成为 AsyncDB 通用操作的统一生命周期层。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;&lt;code&gt;StopSource&lt;/code&gt; / &lt;code&gt;StopToken&lt;/code&gt;&lt;/strong&gt;。取消信号的发布与观察。&lt;code&gt;StopToken&lt;/code&gt; 支持链式父子关系：子 token 观察自身与全部祖先的取消状态，因而一次父级取消可以传播到整棵任务树，而无需维护反向的注册表。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;&lt;code&gt;Quarantine&lt;/code&gt;&lt;/strong&gt;。该组件用于容纳“取消之后才到达的结果”。取消一个已经启动的操作并不等于撤销其物理副作用；操作仍可能完成，只是其原接收者已经退出生命周期。直接写回可能访问失效目标，直接丢弃则可能隐藏资源清理责任，因此隔离区保存结果类别、值或异常，供 scope 在 join 后统一检查。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;&lt;code&gt;OperationState&lt;/code&gt;&lt;/strong&gt;。将上述判定收敛到一处临界区。其状态机为：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;TryStart()   → 若已请求取消，标记 quarantined 并返回 false
             → 否则标记 started 并返回 true
Complete(v)  → 若已完成，直接返回（保证恰好一次）
             → 若已取消，把 v 放入隔离区
             → 否则把 v 应用到目标
Cancel()     → 标记 cancelled；若尚未启动，立即进入隔离
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;关键在于 &lt;code&gt;Complete&lt;/code&gt; 中&quot;是否取消&quot;的判定与&quot;应用结果&quot;必须处于同一个决策点内：若先判定后应用，判定与应用之间的取消会导致结果被写入一个已失效的目标。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;&lt;code&gt;TaskScope&lt;/code&gt;&lt;/strong&gt;。为一组子任务提供统一的取消源、&lt;code&gt;in-flight&lt;/code&gt; 计数与 join 点。&lt;/p&gt;
&lt;h2&gt;18. 存储侧耦合：写路径、读视图与 compaction&lt;/h2&gt;
&lt;p&gt;调度器的形状很大程度上由存储引擎的读写路径决定。&lt;/p&gt;
&lt;h3&gt;18.1 写路径&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;AsyncDB::PutAsync / WriteAsync
  → WriteCoordinator（每 DB 一个）
  → db_write_executor（SerialExecutor，1 线程）
  → 分组：至多 128 个请求 / 1 MiB，且 sync 属性相同
  → PlanWriteGroupForCoordinator
  → AsyncWalWriter：append → 可选 fsync
  → 回到 db_write_executor
  → CommitWriteGroupForCoordinator
  → 完成请求并恢复等待者
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;code&gt;WriteCoordinator&lt;/code&gt; 自身还以 &lt;code&gt;queue_&lt;/code&gt;、&lt;code&gt;drain_scheduled_&lt;/code&gt; 与 &lt;code&gt;group_in_flight_&lt;/code&gt; 表达调度状态。&lt;code&gt;group_in_flight_&lt;/code&gt; 保证任一时刻至多有一个写组处于 WAL 阶段；&lt;code&gt;drain_scheduled_&lt;/code&gt; 防止多个调用者重复向串行执行器提交 &lt;code&gt;Drain()&lt;/code&gt;。这两个状态与 &lt;code&gt;SerialExecutor&lt;/code&gt; 的单 worker 共同建立单写者不变量。&lt;/p&gt;
&lt;p&gt;WAL I/O 并不会一直占用写 worker。规划完成后，&lt;code&gt;AsyncWalWriter&lt;/code&gt; 通过 &lt;code&gt;IoDispatcher&lt;/code&gt; 异步提交 append 与可选 fsync；完成回调再把提交阶段送回 &lt;code&gt;db_write_executor&lt;/code&gt;。因此，“任一时刻至多有一个写组处于 &lt;code&gt;in flight&lt;/code&gt; 状态”是协调器的显式策略，而不是串行线程被同步 I/O 被动占用的结果。&lt;/p&gt;
&lt;p&gt;写组的顺序约束是严格的：&lt;strong&gt;规划序列号 → WAL 追加 → 可选 sync → 应用到 memtable → 发布可见序列 → 完成请求&lt;/strong&gt;。协调器在恢复当前组的客户端协程之前，先预留并提交下一组 WAL，使客户端续体可以与下一次 I/O 重叠，但这不会改变可见序列的发布顺序。分组上限与 &lt;code&gt;sync&lt;/code&gt; 属性边界共同限制批次大小和持久性语义。&lt;/p&gt;
&lt;h3&gt;18.2 读路径&lt;/h3&gt;
&lt;p&gt;读路径经历了三个阶段：独占互斥量 → 读写锁 → 发布式读视图。&lt;/p&gt;
&lt;p&gt;第三阶段的动因见第 6 节：读写锁的竞争随并发度放大，成为串行化点。当前发布式读视图并非简单地延迟两代后回收，而是使用 &lt;strong&gt;64 个分片读计数器与两个 epoch&lt;/strong&gt; 关闭“读取指针后对象被释放”的窗口。&lt;/p&gt;
&lt;p&gt;点读首先读取 &lt;code&gt;point_read_epoch_&lt;/code&gt;，递增该 epoch 下当前线程所属分片的计数器，再以 acquire 语义加载 &lt;code&gt;super_version_&lt;/code&gt;。如果期间 epoch 改变，或视图记录的 epoch 与当前 epoch 不一致，则撤销计数并重试：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;读 epoch e
  → readers[e][shard]++
  → load(super_version)
  → 再次验证 epoch 与 view.point_read_epoch
  → 成功：无锁读取 mem / imm / Version
  → readers[e][shard]--
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;发布者在持有 &lt;code&gt;DBImpl::mutex_&lt;/code&gt; 独占锁时构造新 &lt;code&gt;SuperVersion&lt;/code&gt;，并仅在另一个 epoch 的全部分片计数为零时切换过去、回收该 epoch 上一次使用时退休的视图。随后通过 &lt;code&gt;super_version_.exchange(..., acq_rel)&lt;/code&gt; 发布新视图。若备用 epoch 尚未空闲，发布仍可进行，但继续沿用当前 epoch，并推迟回收。&lt;/p&gt;
&lt;p&gt;因此，点读不需要对 &lt;code&gt;SuperVersion&lt;/code&gt; 做逐次引用计数；跨调用存活的迭代器仍显式持有引用。该方案接近轻量级 epoch reclamation：它以固定数量的分片原子计数换取无共享锁的点读，并把回收判断集中在低频发布路径。&lt;/p&gt;
&lt;h3&gt;18.3 Compaction 生命周期&lt;/h3&gt;
&lt;p&gt;&lt;code&gt;CompactionController&lt;/code&gt; 把后台 compaction 的生命周期结构化，其状态被拆分为两个独立的布尔量：&lt;code&gt;work_requested_&lt;/code&gt;（已观察到需求）与 &lt;code&gt;lane_active_&lt;/code&gt;（已排队或正在运行）。单飞语义由此自然成立——请求只翻转前者，提交时若后者已为真则丢弃重复提交。&lt;/p&gt;
&lt;p&gt;取消检查点有三个，各自的语义不同：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;stateDiagram-v2
    [*] --&amp;gt; Idle: DB 打开
    Idle --&amp;gt; Submitted: ScheduleIfNeeded()
    Submitted --&amp;gt; Running: TrySubmitLocked()
    Running --&amp;gt; Checkpoint: 检查取消
    Checkpoint --&amp;gt; Aborted: 尚无副作用 → 直接中止
    Checkpoint --&amp;gt; Cleanup: 输出已落盘 → 安装结果后不再调度
    Running --&amp;gt; Done: 正常完成
    Aborted --&amp;gt; Idle: OnWorkFinished()
    Cleanup --&amp;gt; Idle: 安装 manifest 变更
    Done --&amp;gt; Idle: OnWorkFinished() 可能重新调度
&lt;/code&gt;&lt;/pre&gt;
&lt;ol&gt;
&lt;li&gt;&lt;strong&gt;启动前&lt;/strong&gt;：在挑选 compaction 之前停止，无任何副作用&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;提交前&lt;/strong&gt;：在写 manifest 之前停止，释放输入、唤醒写者&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;提交后&lt;/strong&gt;：输出已在磁盘上，仍需安装 manifest 变更，但抑制后续调度&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;第三个检查点最容易被遗漏：在输出文件已经创建之后直接中止，会留下不被任何版本引用、也不会被清理的孤儿文件。&lt;/p&gt;
&lt;h2&gt;19. 关机协议与所有权&lt;/h2&gt;
&lt;p&gt;&lt;code&gt;CpuThreadPool&lt;/code&gt; 的关机契约固定在头文件中：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;// - Shutdown Protocol:
//   - Exit() sets exit_flag_, wakes all threads.
//   - Draining: Dispatcher threads and worker threads drain their respective queues before exiting.
//   - Late Submissions: Submissions during shutdown from threads *external* to this scheduler
//     are unsupported. However, submissions from jobs already running on this scheduler&apos;s
//     worker threads are supported and drained.
//   - Delayed Tasks: Pending SubmitAfter tasks are promoted to immediate execution during shutdown.
//   - Destruction: Destructor calls Exit() and joins all threads.
//   - Exception Policy: Any exception escaping a Job is a bug; the scheduler catches
//     and calls std::terminate() (fail-fast).
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;四个阶段：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;① 设置 exit_flag_，释放所有信号量
② join 前把延时任务残留推给 worker（否则它们再无机会被消费）
③ join 定时线程 → join 优先级线程 → join 全部 worker
④ 单线程不动点排空
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;第四阶段必须是不动点循环，因为排空过程中执行的任务可能再次提交：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;while (work_remains) {
    work_remains = false;
    DrainLazyQueueToEmpty(work_remains);
    DrainPriorityQueueToEmpty(work_remains);
    DrainWorkerLocalQueues(work_remains);
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;析构线程在此期间临时把 &lt;code&gt;current_scheduler_&lt;/code&gt; 指向自身，使排空中的重入提交能够通过调试期断言。&lt;/p&gt;
&lt;p&gt;三类执行器的关机策略并不完全相同。&lt;code&gt;SerialExecutor&lt;/code&gt; 设置 &lt;code&gt;stopping_&lt;/code&gt; 后唤醒 worker；worker 仅在队列为空时退出，因此已入队工作会被排空。&lt;code&gt;BlockingExecutor&lt;/code&gt; 设置原子停止标志，释放每个 shard 的 semaphore，等待 worker 在所有 shard 均为空后退出，并在 join 后防御性地顺序排空残留任务。&lt;/p&gt;
&lt;p&gt;&lt;code&gt;BlockingExecutor&lt;/code&gt; 还明确禁止从自身 worker 内析构：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;if (IsCurrentWorker()) { std::terminate(); }
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;原因不仅是 self-join 会失败，还在于 worker 当前正在使用执行器的 shard、停止标志与线程容器。简单 detach 不能延长这些对象的生命周期。因此，当前实现将 worker 内析构视为违反所有权契约，并采取失败快速策略。历史版本曾讨论过把协调状态提升到共享堆对象的方案，但该方案不属于当前实现。&lt;/p&gt;
&lt;p&gt;&lt;code&gt;AsyncRuntime&lt;/code&gt; 的成员顺序为这些协议提供了静态保障：&lt;code&gt;serial_file_executor_&lt;/code&gt; 和 &lt;code&gt;compaction_executor_&lt;/code&gt; 先停止，随后 &lt;code&gt;io_dispatcher_&lt;/code&gt; 停止 pump，最后才销毁其回落所依赖的 &lt;code&gt;blocking_io_executor_&lt;/code&gt;。外部 CPU 池不在该析构链内，调用者必须保证它晚于运行时退出。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;异常策略&lt;/strong&gt;为失败快速：所有执行点包裹 &lt;code&gt;try { job(); } catch (...) { std::terminate(); }&lt;/code&gt;。依据是任何逃逸出作业的异常都意味着调用方违反了契约，此时继续运行只会把损坏状态扩散到其他任务。&lt;/p&gt;
&lt;h2&gt;20. 可观测性&lt;/h2&gt;
&lt;p&gt;调度器自带六个计数器（编译期开关，默认关闭）：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;std::atomic&amp;lt;uint64_t&amp;gt; foreground_fastpath_submits;   // 绕过分发器的提交
std::atomic&amp;lt;uint64_t&amp;gt; foreground_fallback_submits;   // 回落到优先队列的提交
std::atomic&amp;lt;uint64_t&amp;gt; steal_attempts;                // TrySteal 调用次数
std::atomic&amp;lt;uint64_t&amp;gt; steal_successes;               // 实际转移了作业的次数
std::atomic&amp;lt;uint64_t&amp;gt; worker_local_jobs_completed;   // 从自身队列完成
std::atomic&amp;lt;uint64_t&amp;gt; stolen_jobs_completed;         // 被偷取后完成
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;这六个数有固定的读法：&lt;/p&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;观察量&lt;/th&gt;
&lt;th&gt;期望&lt;/th&gt;
&lt;th&gt;偏离的含义&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;fallback_submits&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;接近 0&lt;/td&gt;
&lt;td&gt;快路径在某些场景下未生效&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;steal_successes / steal_attempts&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;个位数百分比&lt;/td&gt;
&lt;td&gt;偏高说明前台分发本身不均衡&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;stolen / worker_local&lt;/code&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;pre&gt;&lt;code&gt;std::atomic&amp;lt;std::size_t&amp;gt; cancelled_before_start;        // 启动前即被取消
std::atomic&amp;lt;std::size_t&amp;gt; cooperative_checkpoint_cancel; // 在协作检查点被取消
std::atomic&amp;lt;std::size_t&amp;gt; late_completion_quarantined;   // 取消后到达的结果被隔离
std::atomic&amp;lt;std::size_t&amp;gt; fallback_to_blocking_count;    // reactor 回落到阻塞路径
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;计数器默认关闭是一个有依据的决定：在当前仪表密度下其开销约为 15%，对于一个以微秒计的热路径而言不可忽略。可观测性与零开销之间的取舍在此偏向后者，代价是诊断时需要重新编译。&lt;/p&gt;
&lt;h2&gt;21. 测试契约&lt;/h2&gt;
&lt;p&gt;调度器的正确性由六个测试文件固定，共 57 个测试，全部在 ASan / LSan / UBSan 下运行：&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;scheduler_shutdown_test&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;15&lt;/td&gt;
&lt;td&gt;四阶段排空、延时任务提升、worker 重入提交&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;scheduler_context_test&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;12&lt;/td&gt;
&lt;td&gt;Context 实例身份、跨池 context 回落&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;scheduler_invariant_test&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;8&lt;/td&gt;
&lt;td&gt;队列与负载计数不变量&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;scheduler_pending_regression_test&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;8&lt;/td&gt;
&lt;td&gt;偷取批次排空后的重新登记&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;scheduler_exceptions_test&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;8&lt;/td&gt;
&lt;td&gt;失败快速&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;scheduler_stress_test&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;6&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;这一方针的依据是可判定性：一个只能通过&quot;重复运行足以碰到&quot;来复现的并发缺陷，无法用于验证修复是否有效——修复后仍然可能只是把复现概率降低了。确定性屏障把竞态的两端固定在可控的顺序上，使得&quot;修复前必然失败、修复后必然通过&quot;成为可验证的性质。&lt;/p&gt;
&lt;p&gt;需要注意的一类干扰是测试自身的缺陷。例如某个回归测试曾假设&quot;两次连续提交在双 worker 池上必然落到不同 worker&quot;，而负载计数器的递减与选择器的读取本身存在竞态，两次探测合法地可能返回同一个 worker。又如一份 sanitizer 报告指向读路径的原子加载，实际原因是测试通过一个已释放的 &lt;code&gt;unique_ptr&lt;/code&gt; 中取出的裸指针继续调用方法——即测试自身违反了所有权契约，而非被测代码存在缺陷。&lt;/p&gt;
&lt;p&gt;另有一类工具层面的假阳性需要预先约定写法：协程在另一线程恢复时，若捕获了发起线程栈上的局部变量，AddressSanitizer 会报告 &lt;code&gt;stack-use-after-scope&lt;/code&gt;。这是已知的工具局限（其提示中亦说明&quot;若程序使用了自定义的栈展开机制，这可能是假阳性&quot;）。约定是协程测试不使用带捕获的 lambda，而把状态作为显式参数传入立即调用的协程 lambda。&lt;/p&gt;
&lt;h2&gt;22. 量化画像&lt;/h2&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;维度&lt;/th&gt;
&lt;th&gt;值&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;物理执行源&lt;/td&gt;
&lt;td&gt;6（CPU 池 + 2 个 BlockingExecutor + compaction lane + 2 个 SerialExecutor）&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;默认线程数&lt;/td&gt;
&lt;td&gt;CPU 池 N + 2 分发；DB 读 4；阻塞 I/O 4；compaction 1；DB 写 1；有序文件 1；reactor pump 1&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;前台提交路径上的锁&lt;/td&gt;
&lt;td&gt;1（目标 worker 的队列锁）&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;前台提交路径上的线程切换&lt;/td&gt;
&lt;td&gt;0&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;每作业堆分配&lt;/td&gt;
&lt;td&gt;0（捕获集 ≤ 128 字节时）&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;一般 &lt;code&gt;AsyncOp&lt;/code&gt; 完成后的额外队列跳转&lt;/td&gt;
&lt;td&gt;0（文件读恢复为 1；写组有独立状态链）&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;热路径上的虚分派&lt;/td&gt;
&lt;td&gt;0&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;调度器测试&lt;/td&gt;
&lt;td&gt;57，全部在 ASan/LSan/UBSan 下&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p&gt;与初始设计对照，前台提交路径的锁数量从 4 降至 1，线程切换从 2 降至 0，作业的堆分配从每次一次降至零。这三项变化解释了实测中上下文切换下降 95% 的机制来源。&lt;/p&gt;
&lt;hr /&gt;
&lt;h2&gt;23. 局限与未决问题&lt;/h2&gt;
&lt;h3&gt;23.1 写停顿占用唯一的写 worker&lt;/h3&gt;
&lt;p&gt;调用链完全位于同一线程上：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;SerialExecutor(1) → WriteCoordinator::Drain() → 规划写组
                  → 独占锁 → MakeRoomForWrite()
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;而 &lt;code&gt;MakeRoomForWrite&lt;/code&gt; 有三个阻塞分支：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;if (allow_delay &amp;amp;&amp;amp; files(0).size() &amp;gt;= kL0_SlowdownWritesTrigger) {   // 8
    lock.unlock(); env_-&amp;gt;SleepForMicroseconds(1000); lock.lock();     // 1 ms 睡眠
} else if (imm_ != nullptr) {
    background_work_finished_signal_.wait(lock);                      // 等待 flush
} else if (files(0).size() &amp;gt;= kL0_StopWritesTrigger) {                // 12
    background_work_finished_signal_.wait(lock);                      // 等待 L0 排干
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;其后果是唯一的写 worker 被占用。但它不占用读、CPU 续体与 compaction——这正是物理隔离所提供的保证：&lt;strong&gt;故障域被限制在一条 lane 内&lt;/strong&gt;。&lt;/p&gt;
&lt;p&gt;这是有意保留的边界。把 room-making 的等待改造为完整的事件驱动状态机，涉及写提交目标的捕获与可见性发布的重新设计，其规模远大于表面所见。当前的约束是：不从这里开始。&lt;/p&gt;
&lt;p&gt;需要区分清楚的是被占用的对象。&lt;code&gt;condition_variable_any::wait(lock)&lt;/code&gt; 在等待期间会释放互斥量，因此这里滞留的是&lt;strong&gt;线程&lt;/strong&gt;而非&lt;strong&gt;互斥量&lt;/strong&gt;——其他持有该互斥量才能推进的路径不受阻塞，唯独写 lane 的单个 worker 被占住。二者的表象接近（写入停滞），但修复方向相反：前者需要把等待改为异步续体，后者才需要缩短临界区。&lt;/p&gt;
&lt;h3&gt;23.2 优先级只作用于回落队列&lt;/h3&gt;
&lt;p&gt;当前 &lt;code&gt;PriorityLoop&lt;/code&gt; 从中央优先队列取出任务后，将其推送到近似负载最低的 worker。优先级因此只决定中央队列中的取出顺序；任务进入 worker-local deque 后，不会抢占已经运行或已在本地排队的普通任务。&lt;/p&gt;
&lt;p&gt;这是一种队列选择规则，而不是端到端服务等级保证。调用者不能由较高的 &lt;code&gt;priority&lt;/code&gt; 推导出确定的完成先后或延迟上界。当前生产路径也没有使用非零优先级，因此这一接口主要由兼容性与测试覆盖维持。&lt;/p&gt;
&lt;h3&gt;23.3 CPU 池的实际职责收缩&lt;/h3&gt;
&lt;p&gt;在当前的生产路径上，&lt;code&gt;CpuThreadPool&lt;/code&gt; 的主要职责是文件读取完成后的回调与协程恢复，也承接少量即时错误和空结果的异步完成。亲和提交、延时提交、非零优先级提交与 context 捕获没有主要生产调用点，它们主要由测试与 benchmark 覆盖。&lt;/p&gt;
&lt;p&gt;这是一个应当被正视的架构事实：一个具备四条提交路径、两条分发线程、优先队列与定时队列的组件，实际使用的只有其中一条最简单的路径。它意味着两件事——其一，前面几节描述的大部分机制目前处于&quot;已实现但未被使用&quot;的状态；其二，若未来的异步 SST 读路径落地，CPU 池将重新成为主要的续体承载者，届时这些机制会被激活。在此之前，它们的正确性由测试而非生产负载保证。&lt;/p&gt;
&lt;h3&gt;23.4 已知的不一致&lt;/h3&gt;
&lt;p&gt;&lt;code&gt;CpuThreadPool::WorkThread&lt;/code&gt; 未标注 &lt;code&gt;alignas(64)&lt;/code&gt;，而 &lt;code&gt;BlockingExecutor::Shard&lt;/code&gt; 标注了。伪共享的规避只应用于较新的执行器。前者的 worker 结构包含互斥量、deque、信号量与负载计数，相邻 worker 的计数字段可能落入同一缓存行。这一差异尚未量化。&lt;/p&gt;
&lt;h3&gt;23.5 尚未实施的工作&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;完全异步的 room-making 等待&lt;/li&gt;
&lt;li&gt;允许多个写组的 WAL I/O 同时处于 &lt;code&gt;in flight&lt;/code&gt; 状态&lt;/li&gt;
&lt;li&gt;&lt;code&gt;sync=false&lt;/code&gt; 请求搭载在 &lt;code&gt;sync=true&lt;/code&gt; 的 fsync 组之后&lt;/li&gt;
&lt;li&gt;异步的 MANIFEST 写入与 SST flush/compaction 文件写入&lt;/li&gt;
&lt;li&gt;通过 TableCache 与 Table 迭代器的真正异步 SST / cache-miss 读&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;最后一项是最重要的。当前的约束记录为：&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;不应为了解决异步 SST 读而增加一条读 lane。问题不在于 lane 的数量，而在于如何使存储 I/O 处于 &lt;code&gt;in flight&lt;/code&gt; 状态时不占用 worker。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h2&gt;24. 结论&lt;/h2&gt;
&lt;p&gt;Prism 的调度系统最终形成了三条相互补充的设计原则。&lt;/p&gt;
&lt;p&gt;第一，执行资源应当按照工作的阻塞性质与顺序语义划分。DB 读、阻塞文件操作、写协调与 compaction 并非同一种“后台任务”；它们只有在资源需求和正确性约束相同时，才应共享 lane。物理隔离的意义不仅是提高吞吐，还在于限制队头阻塞和故障传播的范围。&lt;/p&gt;
&lt;p&gt;第二，常见路径不应承担低频语义的固定成本。普通提交直接进入 worker-local 队列，优先级与延时任务保留独立的回落路径；一般 &lt;code&gt;AsyncOp&lt;/code&gt; 在完成线程上内联恢复，而文件读完成才经由 CPU 池建立明确的线程边界。每一次队列跳转都应当对应一种可说明的隔离或顺序要求。&lt;/p&gt;
&lt;p&gt;第三，调度器不能脱离存储协议单独设计。&lt;code&gt;WriteCoordinator&lt;/code&gt; 用串行 lane 与显式的 &lt;code&gt;in-flight&lt;/code&gt; 状态维护写组顺序，&lt;code&gt;SuperVersion&lt;/code&gt; 用分片 epoch 计数把引用保护移出点读热点，&lt;code&gt;CompactionController&lt;/code&gt; 以单飞状态和停止 token 管理后台生命周期。这里的调度机制实质上是存储不变量的执行形式。&lt;/p&gt;
&lt;p&gt;由此可以得到一个更一般的判断：调度架构的核心并不是选择某一种队列，而是决定哪些工作允许共享执行资源、哪些顺序必须成为显式契约，以及完成事件穿过线程边界时由谁继续推进状态机。只有在这些关系被定义以后，互斥量、semaphore、工作窃取与 io_uring 才有明确的位置。&lt;/p&gt;
</content:encoded><category>Concurrency</category><category>CPP</category><category>Scheduler</category><category>Prism</category><category>Performance</category></item><item><title>缺失的边：从 Lamport Happens-Before 到结构化并发</title><link>https://puji4810.github.io/2026/02/27/Lamport-Happens-Before/</link><guid isPermaLink="true">https://puji4810.github.io/2026/02/27/Lamport-Happens-Before/</guid><description>并发编程的复杂性往往源于对执行顺序的误解。本文通过事件图（Event Graph）视角，重新审视 Lamport 提出的 Happens-Before 原则，并探讨其在现代语言内存模型与结构化并发中的演进。</description><pubDate>Fri, 27 Feb 2026 00:00:00 GMT</pubDate><content:encoded>&lt;p&gt;1978 年，Leslie Lamport 在 &lt;em&gt;Communications of the ACM&lt;/em&gt; 上发表了一篇只有八页的论文——&lt;a href=&quot;https://lamport.azurewebsites.net/pubs/time-clocks.PDF&quot;&gt;&lt;em&gt;Time, Clocks, and the Ordering of Events in a Distributed System&lt;/em&gt;&lt;/a&gt;。这篇论文后来获得了 PODC 最具影响力论文奖（2000 年）和 ACM SIGOPS 名人堂奖（2007 年）。&lt;/p&gt;
&lt;p&gt;论文的核心贡献是一个看似简单的概念：&lt;strong&gt;happens-before&lt;/strong&gt;（先发生于）。并发执行可以被建模为事件图，其中节点是操作，边是因果关系。本文的核心观点是：并发往往存在于那些“缺失的边”之中，而通过显式地补齐这些边，我们可以更清晰地理解数据竞争与结构化并发。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;graph LR
    subgraph NodeTypes[&quot;节点类型&quot;]
        R((&quot;R: 读&quot;))
        W((&quot;W: 写&quot;))
        SP{{&quot;Spawn&quot;}}
        AW{{&quot;Await&quot;}}
        LK[&quot;Lock/Unlock&quot;]
        SD[&quot;Send/Recv&quot;]
    end
    subgraph EdgeTypes[&quot;边类型&quot;]
        direction LR
        A((&quot;a&quot;)) --&amp;gt;|&quot;程序序&quot;| B((&quot;b&quot;))
        C((&quot;c&quot;)) -.-&amp;gt;|&quot;synchronizes-with&quot;| D((&quot;d&quot;))
        E((&quot;e&quot;)) ==&amp;gt;|&quot;spawn/join&quot;| F((&quot;f&quot;))
    end
&lt;/code&gt;&lt;/pre&gt;

&lt;h2&gt;事件图与偏序 (Event Graph &amp;amp; Partial Order)&lt;/h2&gt;
&lt;p&gt;当我们谈论并发程序的执行时，直觉上往往会诉诸“物理时间”：操作 A 是不是在操作 B 之前发生的？然而，在分布式系统和多核处理器中，全局一致的物理时间（Physical Time）既难以测量，也并非逻辑上的必需品。Lamport 在其 1978 年的经典论文中指出：&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;&quot;A distributed system consists of a collection of distinct processes which are spatially separated, and which communicate with one another by exchanging messages... A system is distributed if the message transmission delay is not negligible compared to the time between events in a single process.&quot; (Lamport 1978, p.558)&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;这意味着，分布式系统（甚至可以推广到单机内的多核执行）的本质在于通信延迟的存在，使得我们无法通过单一的全局时钟来判定所有事件的顺序。在分布式系统中，&quot;时间&quot;是没有全局意义的。每台机器有自己的时钟，这些时钟不可能完美同步。&lt;/p&gt;
&lt;p&gt;为此，我们需要将执行过程建模为一个&lt;strong&gt;事件图（Event Graph）&lt;/strong&gt;。每一个执行步骤（如读写内存、发送消息、启动线程）都被视为一个节点。节点之间的连线代表了因果关联。这种由节点和因果边构成的图是一个有向无环图（DAG）。通过这种建模，并发程序的执行逻辑不再是一条线性的时间轴，而是一张复杂的网。在这种视角下，所谓的“顺序”不再是绝对的，而是一种偏序关系（Partial Order），即“因果先行”。&lt;/p&gt;
&lt;h2&gt;Happens-Before 的三个条件&lt;/h2&gt;
&lt;p&gt;为了在缺乏全局时钟的系统中建立顺序感，Lamport 定义了 &lt;strong&gt;Happens-Before&lt;/strong&gt; 关系（记作 $\to$）。这是在事件集合上定义的一种偏序关系，它不要求任意两个事件都有确定的先后顺序，但保证了因果链条的严谨性。根据 Lamport (1978, p.559) 的定义，这个关系是满足以下三个条件的最小关系：&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;&quot;Definition. The relation &apos;---&amp;gt;&apos; on the set of events of a system is the smallest relation satisfying the following three conditions: (1) If a and b are events in the same process, and a comes before b, then a → b. (2) If a is the sending of a message by one process and b is the receipt of the same message by another process, then a → b. (3) If a → b and b → c then a ---&amp;gt; c.&quot;&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;我们可以将这三个条件直观地理解为：&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;程序序（C1, Program Order）&lt;/strong&gt;：在同一个进程（或线程）内，代码执行的先后顺序自然构成了 Happens-Before 关系。&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;同步序（C2, Synchronization Order）&lt;/strong&gt;：如果进程 P1 发送了一条消息，而 P2 接收了这条消息，那么“发送”这一动作一定 Happens-Before “接收”。在内存模型中，这对应于 release 操作与 acquire 操作之间的同步。&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;传递性（Transitivity）&lt;/strong&gt;：如果 $a \to b$ 且 $b \to c$，那么 $a \to c$。&lt;/p&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;基于这个定义，我们可以给**并发（Concurrency）**下一个非常精确的定义：如果两个事件 $a$ 和 $b$ 之间既没有 $a \to b$ 也不存在 $b \to a$（即 $a \nrightarrow b$ 且 $b \nrightarrow a$），那么我们就称 $a$ 和 $b$ 是并发的（记作 $a \parallel b$）。这意味着它们在因果图中没有路径相连，处于不同的分支上。值得注意的是，&lt;strong&gt;并发并不意味着“同时发生”&lt;/strong&gt;，而是指它们在逻辑顺序上是互不干扰的。并发的本质，往往就隐藏在那些“缺失的边”中。&lt;/p&gt;
&lt;p&gt;以经典的 Message Passing (MP) 模型为例，如下方的 Mermaid 图 (D2) 所示。如果缺失了从 P1 的 &lt;code&gt;send&lt;/code&gt; 到 P2 的 &lt;code&gt;recv&lt;/code&gt; 这条虚线边，那么 P1 的数据准备（a, b）与 P2 的数据使用（d, e）之间就没有任何 Happens-Before 路径。在共享内存类比中，这种边的缺失会让跨线程的访问缺乏顺序保证；如果访问的是同一个非原子对象并且至少一方是写，就会形成数据竞争（Data Race）。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;graph TB
    subgraph P1[&quot;进程 P1&quot;]
        direction TB
        a1((&quot;a&quot;)) --&amp;gt; b1((&quot;b&quot;)) --&amp;gt; c1((&quot;send&quot;))
    end
    subgraph P2[&quot;进程 P2&quot;]
        direction TB
        a2((&quot;recv&quot;)) --&amp;gt; b2((&quot;d&quot;)) --&amp;gt; c2((&quot;e&quot;))
    end
    c1 -.-&amp;gt;|&quot;消息&quot;| a2
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;时钟条件与逻辑时钟&lt;/h2&gt;
&lt;p&gt;既然物理时间不可靠，我们能否通过某种逻辑上的“计数器”来捕捉这种 Happens-Before 关系？这就是 Lamport 提出的逻辑时钟（Logical Clock）。每个进程维护一个本地计数器，并在事件发生或接收消息时进行更新。由此产生的时间戳需要满足所谓的&lt;strong&gt;时钟条件（Clock Condition）&lt;/strong&gt;：&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;&quot;Clock Condition. For any events a, b: if a---&amp;gt; b then C(a) &amp;lt; C(b).&quot; (Lamport 1978, p.560)&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;这意味着，如果两个事件之间存在因果关系，那么它们的时间戳一定反映了这种顺序。然而，时钟条件的一个关键性质是它的&lt;strong&gt;不可逆性&lt;/strong&gt;：如果 $C(a) &amp;lt; C(b)$，我们并不能推导出 $a \to b$。这正是逻辑时钟的局限性所在。两个完全没有因果关系的并发事件，也可能因为本地计数器的更新步调，而在数值上呈现出某种大小关系。&lt;/p&gt;
&lt;p&gt;例如，进程 P1 在本地执行了一万个事件（都是纯计算，没有通信），其时间戳已经累加到 10000。进程 P2 刚启动，时间戳为 1。P2 的某个事件时间戳为 1，P1 的某个事件时间戳为 5000，但这两个事件之间可能毫无因果关系。&lt;/p&gt;
&lt;h3&gt;实现算法 (IR1/IR2)&lt;/h3&gt;
&lt;p&gt;Lamport 给出了一套简单的规则来维护这个计数器：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;IR1（进程内推进）&lt;/strong&gt;：每个事件发生时，计数器自增：&lt;code&gt;local_clock += 1&lt;/code&gt;。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;IR2（消息接收时同步）&lt;/strong&gt;：接收到带时间戳 $T_m$ 的消息时：&lt;code&gt;local_clock = max(local_clock, Tm) + 1&lt;/code&gt;。&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;这个 &lt;code&gt;max + 1&lt;/code&gt; 保证了接收事件的时间戳严格大于发送事件的时间戳，从而满足 C2。&lt;/p&gt;
&lt;h3&gt;全序扩展与外部因果性&lt;/h3&gt;
&lt;p&gt;偏序可以被扩展为全序：只需用进程 ID 打破时间戳相同时的平局。&lt;code&gt;a ⇒ b&lt;/code&gt; iff $C(a) &amp;lt; C(b)$，或 $C(a) = C(b)$ 且 $pid(a) &amp;lt; pid(b)$。Lamport 在论文中说这个全序是 &lt;strong&gt;&quot;somewhat arbitrary&quot;&lt;/strong&gt;（有些随意的），因为它仅仅是一个约定。&lt;/p&gt;
&lt;p&gt;另一个限制是&lt;strong&gt;外部因果性&lt;/strong&gt;：如果一个人打电话告诉另一台机器上的操作员&quot;我刚看到事件 a 发生了&quot;，这条因果链无法被系统捕获。这是形式模型和物理现实之间永恒的张力。&lt;/p&gt;
&lt;p&gt;这种“时间戳不代表因果”的现象，再次提醒我们 Happens-Before 是关于逻辑拓扑的，而非绝对时间的。虽然向量时钟（Vector Clocks）可以通过维护更多的元数据来解决逆命题问题：用向量偏序判断 $VC(a) &amp;lt; VC(b) \iff a \to b$，但这通常会带来额外的内存开销 &lt;a href=&quot;https://puji4810.github.io/2026/02/27/Lamport-Happens-Before/%E5%90%91%E9%87%8F%E6%97%B6%E9%92%9F%E9%80%9A%E8%BF%87%E5%9C%A8%E6%AF%8F%E4%B8%AA%E8%BF%9B%E7%A8%8B%E4%B8%AD%E5%AD%98%E5%82%A8%E4%B8%80%E4%B8%AA%E6%95%B0%E7%BB%84%E6%9D%A5%E8%AE%B0%E5%BD%95%E6%89%80%E6%9C%89%E5%B7%B2%E7%9F%A5%E8%BF%9B%E7%A8%8B%E7%9A%84%E6%97%B6%E9%92%9F%E7%8A%B6%E6%80%81%EF%BC%8C%E4%BB%8E%E8%80%8C%E8%83%BD%E5%A4%9F%E5%87%86%E7%A1%AE%E8%AF%86%E5%88%AB%E5%B9%B6%E5%8F%91%E4%BA%8B%E4%BB%B6%E3%80%82&quot;&gt;^1&lt;/a&gt;。&lt;/p&gt;
&lt;h2&gt;从硬件到语言：消息传递 (Hardware to Language: Message Passing)&lt;/h2&gt;
&lt;p&gt;在 Lamport 的原始论文中，消息传递是建立跨进程因果关系的唯一手段。这个视角也可以类比到现代多核处理器：每个核心都有自己的执行观察点，而 release/acquire、锁、屏障等同步操作负责把跨核心的因果边显式接起来。&lt;/p&gt;
&lt;h3&gt;硬件的重排序与弱内存模型&lt;/h3&gt;
&lt;p&gt;现代 CPU 为了性能，会对内存操作进行重排序（如 Store Buffer 的延迟刷新或乱序执行）。不同硬件模型允许的重排序并不相同：x86 的 &lt;strong&gt;TSO（Total Store Order）&lt;/strong&gt; 保留了较强的 store-store 顺序，而 ARM/POWER 这类弱内存模型则需要更明确的屏障或 acquire/release 语义。&lt;/p&gt;
&lt;p&gt;在弱内存模型下，即使两个线程在同一个物理内存上操作，缺失了显式同步也可能打破直觉。例如，线程 2 可能会先观察到 &lt;code&gt;flag&lt;/code&gt; 的更新，却读到 &lt;code&gt;data&lt;/code&gt; 的旧值。这在事件图的角度来看，不是因为线程 1 内部缺少 &lt;code&gt;W(data) -&amp;gt; W(flag)&lt;/code&gt; 的程序序，而是因为 &lt;code&gt;W(flag)&lt;/code&gt; 和 &lt;code&gt;R(flag)&lt;/code&gt; 之间没有建立跨线程的同步边；因此这条程序序无法传递到线程 2 的 &lt;code&gt;R(data)&lt;/code&gt;。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;graph TD
    subgraph T1[&quot;线程 1&quot;]
        direction TB
        w1((&quot;W: data=1&quot;)) --&amp;gt; w2((&quot;W: flag=1&quot;))
    end
    subgraph T2[&quot;线程 2&quot;]
        direction TB
        r1((&quot;R: flag==1&quot;)) --&amp;gt; r2((&quot;R: data==?&quot;))
    end
    w2 -.-&amp;gt;|&quot;缺失的 synchronizes-with&quot;| r1
    w1 ~~~ r2
    linkStyle 2 stroke:red,stroke-width:2px,stroke-dasharray:5
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;C++ 内存模型：补齐缺失的边&lt;/h3&gt;
&lt;p&gt;现代编程语言（如 C++）的内存模型，本质上是在硬件之上提供的一层抽象。在 C++ 中，这种跨线程的边通常是由 &lt;strong&gt;synchronizes-with&lt;/strong&gt;（同步关系）来构建的。当我们使用 &lt;code&gt;std::atomic&lt;/code&gt; 的 Acquire/Release 语义时，一个线程的 Release Store 可以同步于另一个线程对同一变量、且读到该值的 Acquire Load。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;#include &amp;lt;atomic&amp;gt;
#include &amp;lt;thread&amp;gt;
#include &amp;lt;cassert&amp;gt;

std::atomic&amp;lt;int&amp;gt; data{0};
std::atomic&amp;lt;int&amp;gt; flag{0};

// 线程 1: 生产者 (Producer)
void producer() {
    // 1. sequenced-before: data 写入先于 flag 写入
    data.store(42, std::memory_order_relaxed);
    
    // 2. Release store: 建立同步边的起点
    flag.store(1, std::memory_order_release);  // ← release
}

// 线程 2: 消费者 (Consumer)
void consumer() {
    // 3. Acquire load: 建立同步边的终点
    // 当读取到 1 时，与 producer 的 store 构成 synchronizes-with 关系
    while (flag.load(std::memory_order_acquire) != 1) {}  // ← acquire
    
    // 4. 结合传递性，此时 data.store 一定 happens-before 这里的 read
    assert(data.load(std::memory_order_relaxed) == 42); // 在这个程序中安全
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;在 C++ 中，如果对同一个非原子对象的并发读写没有通过这种方式显式建立 Happens-Before 关系，就会构成 &lt;strong&gt;Data Race&lt;/strong&gt;，这在标准中被定义为未定义行为（Undefined Behavior）。如果对象本身是 &lt;code&gt;std::atomic&lt;/code&gt;，即使用 &lt;code&gt;memory_order_relaxed&lt;/code&gt; 也不会形成 C++ 意义上的 data race，但 relaxed 原子操作不提供上面这类跨对象的可见性保证。&lt;/p&gt;
&lt;h2&gt;JMM 中的 happens-before&lt;/h2&gt;
&lt;p&gt;Java 5 引入的内存模型（JSR-133）也直接使用了 &quot;happens-before&quot; 术语。我们可以用 C1/C2 的语言来理解 JMM 的规则：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;程序顺序规则 (C1)&lt;/strong&gt;：同一线程中，前面的操作 happens-before 后面的操作。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;同步规则 (C2)&lt;/strong&gt;：
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;监视器锁规则&lt;/strong&gt;：&lt;code&gt;unlock(m)&lt;/code&gt; happens-before 后续的 &lt;code&gt;lock(m)&lt;/code&gt;。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;volatile 变量规则&lt;/strong&gt;：对 &lt;code&gt;volatile&lt;/code&gt; 变量的写 happens-before 后续对同一变量的读。&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;code&gt;Thread.start()&lt;/code&gt; happens-before 新线程的任何操作。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;线程终止规则&lt;/strong&gt;：线程的所有操作 happens-before &lt;code&gt;Thread.join()&lt;/code&gt; 的返回。&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;结构化并发：让边成为结构 (Structured Concurrency)&lt;/h2&gt;
&lt;p&gt;如果说 MP 模型揭示了由于缺失同步边（Synchronization Edge）导致的读写冲突，那么结构化并发解决的则是生命周期边（Lifetime Edge）的缺失。&lt;/p&gt;
&lt;p&gt;传统的并发（如 &lt;code&gt;detach&lt;/code&gt; 或 &lt;code&gt;Future.then&lt;/code&gt;）会让 happens-before 关系与词法作用域脱节，导致悬垂引用（Dangling Reference）。结构化并发的核心约束是：&lt;strong&gt;子任务的操作必须在其父任务的作用域退出之前全部完成&lt;/strong&gt;。&lt;/p&gt;
&lt;h3&gt;C++ 协程中的结构化&lt;/h3&gt;
&lt;p&gt;在 C++ 协程中，逻辑上建立了一条从子任务完成到父任务继续执行的 Happens-Before 边。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;// ✅ 伪代码 — 结构化并发修复
task&amp;lt;void&amp;gt; parent() {
    std::string data = &quot;hello&quot;;
    co_await with_task_group([&amp;amp;](task_group&amp;amp; g) -&amp;gt; task&amp;lt;void&amp;gt; {
        g.spawn([&amp;amp;data]() -&amp;gt; task&amp;lt;void&amp;gt; {
            co_await async_sleep(1s);
            std::cout &amp;lt;&amp;lt; data;  // ✅ data 保证存活，因为 join 边存在
        });
        co_return;  // task_group 析构时：自动触发 cancel -&amp;gt; join 逻辑
    });
    // 到达此处时，所有子任务在逻辑上已经 Happens-Before 随后的操作
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;strong&gt;关键免责声明&lt;/strong&gt;：&lt;code&gt;join&lt;/code&gt; 建立了生命周期顺序，但可见性（Visibility）取决于同步机制与执行器的实现。&lt;code&gt;co_await&lt;/code&gt; 并不是万能的内存栅栏，它更多地是在拓扑层面补齐了那条缺失的因果边。&lt;/p&gt;
&lt;h3&gt;多语言实践&lt;/h3&gt;
&lt;p&gt;这种“将 happens-before 编码进词法结构”的设计在现代语言中已成趋势：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Java 21 StructuredTaskScope&lt;/strong&gt;：&lt;code&gt;scope.join()&lt;/code&gt; 建立了确定的 happens-before 边。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Python asyncio.TaskGroup&lt;/strong&gt;：退出 &lt;code&gt;with&lt;/code&gt; 块时，所有任务已完成，生命周期上可以安全使用结果。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Swift async let&lt;/strong&gt;：&lt;code&gt;await&lt;/code&gt; 建立 happens-before，并发任务的结果在后续可见。&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;概念对应表&lt;/h2&gt;
&lt;p&gt;总结一下 Lamport 1978 的概念与现代系统的对应关系：&lt;/p&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Lamport 1978&lt;/th&gt;
&lt;th&gt;硬件/JMM/C++&lt;/th&gt;
&lt;th&gt;结构化并发&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;进程（process）&lt;/td&gt;
&lt;td&gt;线程 / CPU 核心&lt;/td&gt;
&lt;td&gt;协程 / 任务&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;事件（event）&lt;/td&gt;
&lt;td&gt;内存读写、原子操作&lt;/td&gt;
&lt;td&gt;&lt;code&gt;co_await&lt;/code&gt; 点、任务创建&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;消息发送/接收（C2）&lt;/td&gt;
&lt;td&gt;&lt;code&gt;release&lt;/code&gt; 写 / &lt;code&gt;acquire&lt;/code&gt; 读&lt;/td&gt;
&lt;td&gt;&lt;code&gt;fork&lt;/code&gt; / &lt;code&gt;join&lt;/code&gt;（作用域退出）&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;进程内顺序（C1）&lt;/td&gt;
&lt;td&gt;程序顺序（program order）&lt;/td&gt;
&lt;td&gt;协程内顺序&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;happens-before &lt;code&gt;→&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;JMM/C++ happens-before&lt;/td&gt;
&lt;td&gt;任务完成到父任务继续执行的顺序&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;并发事件 &lt;code&gt;∥&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;无 HB 的并发访问；冲突时形成 data race&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;seq_cst&lt;/code&gt; 全序 / 单个原子对象的修改序&lt;/td&gt;
&lt;td&gt;任务完成顺序&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;h2&gt;&quot;Don&apos;ts&quot; 误区清单&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;Happens-Before $\neq$ 物理时间的先后顺序。&lt;/li&gt;
&lt;li&gt;Lamport 时间戳相等并不代表事件同步。&lt;/li&gt;
&lt;li&gt;C++ 中的 &lt;code&gt;volatile&lt;/code&gt; 不提供同步（Synchronization）语义。&lt;/li&gt;
&lt;li&gt;C++ 中非原子对象上的数据竞争是未定义行为（UB），本质上是因为冲突访问之间缺失了必要的 HB 边。&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;附录: Store Buffering litmus&lt;/h2&gt;
&lt;p&gt;本节讨论 Store Buffering 模型，展示硬件重排序如何挑战直觉。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;// Store Buffering (SB) litmus test
std::atomic&amp;lt;int&amp;gt; x{0}, y{0};

// Thread 1
void thread1() {
    x.store(1, std::memory_order_relaxed);
    int r1 = y.load(std::memory_order_relaxed);
    // r1 == 0 是否可能？
}

// Thread 2
void thread2() {
    y.store(1, std::memory_order_relaxed);
    int r2 = x.load(std::memory_order_relaxed);
    // r2 == 0 是否可能？
}

// 问题：r1 == 0 &amp;amp;&amp;amp; r2 == 0 是否同时发生？
// x86-TSO：可能。每个核心可能在 store 仍停留于自己的 Store Buffer 时继续执行后续 load
// ARM/POWER：也可能，而且弱内存模型还允许更多重排序
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;结语：一个概念的生命力&lt;/h2&gt;
&lt;p&gt;Lamport 在 1978 年提出 happens-before，是为了解决分布式系统中&quot;事件先后&quot;的形式化问题。并发问题的核心通常不是物理时间上的谁先谁后，而是共享状态、消息和任务生命周期之间是否存在足够的因果边。Happens-before 正是描述这些因果边的基础工具。&lt;/p&gt;
&lt;p&gt;理解了 happens-before，你就理解了为什么 &lt;code&gt;volatile&lt;/code&gt; 写在 Java 中有特殊含义，为什么 C++ 的 &lt;code&gt;memory_order_acquire&lt;/code&gt; 叫&quot;acquire&quot;，为什么结构化并发的 scope 退出需要等待所有子任务完成。这些不是孤立的语言特性，它们是同一个数学概念在不同层次的投影。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;参考资料&lt;/strong&gt;&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;Leslie Lamport, &quot;Time, Clocks, and the Ordering of Events in a Distributed System,&quot; &lt;em&gt;Communications of the ACM&lt;/em&gt;, vol. 21, no. 7, pp. 558–565, July 1978. DOI: &lt;a href=&quot;https://doi.org/10.1145/359545.359563&quot;&gt;10.1145/359545.359563&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;Russ Cox, &lt;a href=&quot;https://research.swtch.com/hwmm&quot;&gt;Hardware Memory Models&lt;/a&gt;, 2021&lt;/li&gt;
&lt;li&gt;Eric Niebler, &lt;a href=&quot;https://ericniebler.com/2020/11/08/structured-concurrency/&quot;&gt;Structured Concurrency&lt;/a&gt;, 2020&lt;/li&gt;
&lt;li&gt;JSR-133: Java Memory Model and Thread Specification, 2004&lt;/li&gt;
&lt;li&gt;ISO/IEC 14882:2011 (C++11), §29, Atomic operations library&lt;/li&gt;
&lt;/ol&gt;
</content:encoded><category>Concurrency</category><category>CPP</category><category>Happens-Before</category><category>Distributed Systems</category><category>Memory Model</category></item><item><title>Singleton</title><link>https://puji4810.github.io/2025/12/26/Singleton/</link><guid isPermaLink="true">https://puji4810.github.io/2025/12/26/Singleton/</guid><description>Some notes about Singleton pattern in C++.</description><pubDate>Fri, 26 Dec 2025 00:00:00 GMT</pubDate><content:encoded>&lt;p&gt;我们现在常常见到的单例模式的实现，也是来自 &lt;a href=&quot;https://isocpp.github.io/CppCoreGuidelines/CppCoreGuidelines#i3-avoid-singletons&quot;&gt;CppCoreGuidelines&lt;/a&gt;：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;X&amp;amp; myX()
{
    static X my_x {3};
    return my_x;
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;应该是最简单的一种实现了，简单且线程安全，在第一次执行时才会初始化对象。&lt;/p&gt;

&lt;p&gt;然而，&lt;code&gt;static&lt;/code&gt; 的析构顺序是未保证的，当多个单例相互具有依赖的时候，这种方式就无能为例了。&lt;/p&gt;
&lt;p&gt;&lt;code&gt;CppCoreGuidelines&lt;/code&gt; 里面也提到了&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;Note that the initialization of a local &lt;code&gt;static&lt;/code&gt; does not imply a race condition. However, if the destruction of &lt;code&gt;X&lt;/code&gt; involves an operation that needs to be synchronized we must use a less simple solution.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;可以使用:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;X&amp;amp; myX()
{
    static auto p = new X {3};
    return *p;  // potential leak
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;如果不调用 &lt;code&gt;delete&lt;/code&gt;，那么对象在程序的整个生命周期内，一直有效，就不会收到析构顺序的影响了。
Goole的代码中似乎常有这样的用法，例如 &lt;a href=&quot;https://github.com/google/leveldb/blob/ac691084fdc5546421a55b25e7653d450e5a25fb/util/no_destructor.h#L12-L48&quot;&gt;LevelDB&lt;/a&gt; 中的 &lt;code&gt;NoDestructor&lt;/code&gt; :&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;namespace leveldb {

// Wraps an instance whose destructor is never called.
//
// This is intended for use with function-level static variables.
template &amp;lt;typename InstanceType&amp;gt;
class NoDestructor {
 public:
  template &amp;lt;typename... ConstructorArgTypes&amp;gt;
  explicit NoDestructor(ConstructorArgTypes&amp;amp;&amp;amp;... constructor_args) {
    static_assert(sizeof(instance_storage_) &amp;gt;= sizeof(InstanceType),
                  &quot;instance_storage_ is not large enough to hold the instance&quot;);
    static_assert(std::is_standard_layout_v&amp;lt;NoDestructor&amp;lt;InstanceType&amp;gt;&amp;gt;);
    static_assert(
        offsetof(NoDestructor, instance_storage_) % alignof(InstanceType) == 0,
        &quot;instance_storage_ does not meet the instance&apos;s alignment requirement&quot;);
    static_assert(
        alignof(NoDestructor&amp;lt;InstanceType&amp;gt;) % alignof(InstanceType) == 0,
        &quot;instance_storage_ does not meet the instance&apos;s alignment requirement&quot;);
    new (instance_storage_)
        InstanceType(std::forward&amp;lt;ConstructorArgTypes&amp;gt;(constructor_args)...);
  }

  ~NoDestructor() = default;

  NoDestructor(const NoDestructor&amp;amp;) = delete;
  NoDestructor&amp;amp; operator=(const NoDestructor&amp;amp;) = delete;

  InstanceType* get() {
    return reinterpret_cast&amp;lt;InstanceType*&amp;gt;(&amp;amp;instance_storage_);
  }

 private:
  alignas(InstanceType) char instance_storage_[sizeof(InstanceType)];
};

}  // namespace leveldb
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;就是不调用析构函数，希望对象在程序的整个生命周期都有效。&lt;/p&gt;
&lt;p&gt;前几天在微信公众号上看见一篇有关 &lt;a href=&quot;https://mp.weixin.qq.com/s/DHpPOpcUOQ-ebYLlxkhIrQ?poc_token=HCgbTmmjhKJYuYT2idiMgPceuvp7zxE-kDVcvpjN&quot;&gt;union&lt;/a&gt; 的用法的帖子，原来 &lt;code&gt;union&lt;/code&gt; 也能有类似的作用。&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;Absent default member initializers, if any non-static data member of a union has a non-trivial default constructor, copy constructor, move constructor, copy assignment operator, move assignment operator, or destructor, the corresponding member function of the union will be implicitly deleted unless it is user-provided.
在没有默认成员初始化器的情况下，如果 union 的任一非静态数据成员具有非平凡的默认构造函数、拷贝构造函数、移动构造函数、拷贝赋值运行符、移动赋值运算符或析构函数，则其相应的成员函数将被隐式删除（除非用户显式定义）。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;使用的例子是：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;namespace {

template&amp;lt;typename T&amp;gt;
struct constant_init {
    union {
        T obj;
    };
    constexpr constant_init() : obj() {}

    ~constant_init() { /* do nothing, union object is not destroyed */}
};

struct some_class {};

constant_init&amp;lt;some_class&amp;gt; some_class_instantance {};

} // anonymous namespace
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;code&gt;union&lt;/code&gt; 里面根本不会调用 &lt;code&gt;T&lt;/code&gt; 的析构函数。&lt;/p&gt;
&lt;p&gt;又有方法，其实就类似上面 CppCoreGuidelines 里面说的第二种方法了:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;struct T { /*...*/ };

T&amp;amp; instance() {
  static std::once_flag flag;
  alignas(T) static unsigned char buf[sizeof(T)];
  static T* p = nullptr;

  std::call_once(flag, []{
    p = new (buf) T();
  });

  return *p;
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;使用 &lt;code&gt;placement new&lt;/code&gt; 在一个局部的 &lt;code&gt;buf&lt;/code&gt; 里面构造了一个对象，然后返回这个对象。只要我们不去调用析构函数就不会进行析构。&lt;/p&gt;
</content:encoded><category>CPP</category><category>Design Pattern</category></item><item><title>LevelDB Table format</title><link>https://puji4810.github.io/2025/11/01/LevelDB-Tableformat/</link><guid isPermaLink="true">https://puji4810.github.io/2025/11/01/LevelDB-Tableformat/</guid><description>Overview of WriteBatch layout and record encoding for LevelDB table operations.</description><pubDate>Sat, 01 Nov 2025 00:00:00 GMT</pubDate><content:encoded>
&lt;pre&gt;&lt;code&gt;---
config:
  packet:
    bitsPerRow: 32
---
packet-beta
0-63: &quot;sequence: fixed64&quot;
64-95: &quot;count: fixed32&quot;
96-255: &quot;data: record[count]&quot;
&lt;/code&gt;&lt;/pre&gt;
&lt;pre&gt;&lt;code&gt;---
config:
  packet:
    bitsPerRow: 32
---
packet-beta
0-7: &quot;type: kTypeValue&quot;
8-15: &quot;key_len: varint32&quot;
16-47: &quot;key_data: uint8[key_len]&quot;
48-55: &quot;value_len: varint32&quot;
56-87: &quot;value_data: uint8[value_len]&quot;
&lt;/code&gt;&lt;/pre&gt;
&lt;pre&gt;&lt;code&gt;---
config:
  packet:
    bitsPerRow: 32
---
packet-beta
0-7: &quot;type: kTypeDeletion&quot;
8-15: &quot;key_len: varint32&quot;
16-47: &quot;key_data: uint8[key_len]&quot;
&lt;/code&gt;&lt;/pre&gt;
&lt;pre&gt;&lt;code&gt;WriteBatch::rep_ :=
   sequence: fixed64
   count: fixed32
   data: record[count]
record :=
   kTypeValue varstring varstring         |
   kTypeDeletion varstring
varstring :=
   len: varint32
   data: uint8[len]
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;code&gt;PUT&lt;/code&gt; and &lt;code&gt;DELETE&lt;/code&gt; have different &lt;code&gt;record&lt;/code&gt; structure.&lt;/p&gt;
&lt;p&gt;We will use &lt;code&gt;WriteBatch&lt;/code&gt; as the actuall format in our &lt;code&gt;WAL&lt;/code&gt;.&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;sequence&lt;/code&gt; : 8 bytes, the index of sequence&lt;/li&gt;
&lt;li&gt;&lt;code&gt;count&lt;/code&gt; : 4 bytes, the count of operations&lt;/li&gt;
&lt;li&gt;&lt;code&gt;record&lt;/code&gt; : the record of &lt;code&gt;PUT&lt;/code&gt; and &lt;code&gt;DELETE&lt;/code&gt; opreation
&lt;ul&gt;
&lt;li&gt;kTypeValue varstring(key) varstring(value) (for &lt;code&gt;PUT&lt;/code&gt;)&lt;/li&gt;
&lt;li&gt;kTypeDeletion varstring(key) (for &lt;code&gt;DELETION&lt;/code&gt;)&lt;/li&gt;
&lt;li&gt;
&lt;blockquote&gt;
&lt;p&gt;Here, &lt;code&gt;varint32&lt;/code&gt; and &lt;code&gt;uint8[len]&lt;/code&gt; make the &lt;code&gt;varstring&lt;/code&gt;, which can be understanded as [key_len key_value] or [data_len data_value]&lt;/p&gt;
&lt;/blockquote&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
</content:encoded><category>LevelDB</category><category>Prism</category><category>KV</category></item><item><title>LevelDB Logformat</title><link>https://puji4810.github.io/2025/11/01/LevelDB-Logformat/</link><guid isPermaLink="true">https://puji4810.github.io/2025/11/01/LevelDB-Logformat/</guid><description>Structure of LevelDB&apos;s write-ahead log blocks and record fragmentation.</description><pubDate>Sat, 01 Nov 2025 00:00:00 GMT</pubDate><content:encoded>
&lt;pre&gt;&lt;code&gt;---
config:
  packet:
    bitsPerRow: 32
    showBits: true
---
packet-beta
0-31: &quot;checksum (uint32, crc32c)&quot;
32-47: &quot;length (uint16)&quot;
48-55: &quot;type (uint8)&quot;
56-255: &quot;data[0..length-1]&quot;
&lt;/code&gt;&lt;/pre&gt;
&lt;pre&gt;&lt;code&gt;block := record* trailer?
record :=
  checksum: uint32     // crc32c of type and data[] ; little-endian
  length: uint16       // little-endian
  type: uint8          // One of FULL, FIRST, MIDDLE, LAST
  data: uint8[length]
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;Record Fragmentation&lt;/h2&gt;
&lt;p&gt;The log format supports fragmentation for records larger than the available space in a block (32KB).&lt;/p&gt;
&lt;h3&gt;Record Types&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;FULL (1)&lt;/strong&gt;: Complete record fits in one block&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;FIRST (2)&lt;/strong&gt;: First fragment of a multi-block record&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;MIDDLE (3)&lt;/strong&gt;: Middle fragment of a multi-block record&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;LAST (4)&lt;/strong&gt;: Last fragment of a multi-block record&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;ZERO (0)&lt;/strong&gt;: Reserved for padding at end of block&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;Fragmentation Algorithm&lt;/h3&gt;
&lt;p&gt;When writing a record:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;If record fits in current block → write as FULL&lt;/li&gt;
&lt;li&gt;If record spans blocks:
&lt;ul&gt;
&lt;li&gt;First fragment → FIRST type&lt;/li&gt;
&lt;li&gt;Middle fragments → MIDDLE type&lt;/li&gt;
&lt;li&gt;Last fragment → LAST type&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;h3&gt;Example: 50KB Record in 32KB Blocks&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;Block 1 (32KB):
  Header: type=FIRST, length=32753  (32KB - 7 byte header)
  Data: bytes [0..32752]

Block 2 (32KB):
  Header: type=LAST, length=17247   (remaining bytes)
  Data: bytes [32753..49999]
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;Block Boundary Handling&lt;/h3&gt;
&lt;p&gt;When less than 7 bytes remain in a block (not enough for header):&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Pad with zero bytes (type=ZERO)&lt;/li&gt;
&lt;li&gt;Start next record in new block&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;This ensures all records have complete headers.&lt;/p&gt;
</content:encoded><category>LevelDB</category><category>Prism</category><category>KV</category></item><item><title>LevelDB DBformat</title><link>https://puji4810.github.io/2025/11/01/LevelDB-DBformat/</link><guid isPermaLink="true">https://puji4810.github.io/2025/11/01/LevelDB-DBformat/</guid><description>InternalKey and LookupKey formats, value types, sequence numbers, and comparator rules in LevelDB.</description><pubDate>Sat, 01 Nov 2025 00:00:00 GMT</pubDate><content:encoded>&lt;h1&gt;Database Format Design&lt;/h1&gt;

&lt;h2&gt;Internal Key Format&lt;/h2&gt;
&lt;p&gt;LevelDB uses &lt;strong&gt;InternalKey&lt;/strong&gt; instead of raw user keys to support MVCC (Multi-Version Concurrency Control) and snapshots.&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;---
config:
  packet:
    bitsPerRow: 32
---
packet-beta
0-255: &quot;user_key: bytes[user_key_len]&quot;
256-319: &quot;sequence: uint56 (bits 8-63)&quot;
320-327: &quot;type: uint8 (bits 0-7)&quot;
&lt;/code&gt;&lt;/pre&gt;
&lt;pre&gt;&lt;code&gt;InternalKey :=
   user_key: bytes[user_key_len]
   tag: uint64
   
tag := (sequence &amp;lt;&amp;lt; 8) | type

where:
   sequence: uint56 (0 to 2^56 - 1)
   type: uint8 (kTypeDeletion = 0x0, kTypeValue = 0x1)
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;InternalKey Comparison Rules&lt;/h3&gt;
&lt;p&gt;InternalKey comparison follows these rules (in order):&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;Compare user_key&lt;/strong&gt; (ascending order)&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;If &lt;code&gt;user_key_a &amp;lt; user_key_b&lt;/code&gt;, then &lt;code&gt;InternalKey_a &amp;lt; InternalKey_b&lt;/code&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;If user_key equal, compare sequence&lt;/strong&gt; (descending order)&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Newer entries (larger sequence) come first&lt;/li&gt;
&lt;li&gt;If &lt;code&gt;seq_a &amp;gt; seq_b&lt;/code&gt;, then &lt;code&gt;InternalKey_a &amp;lt; InternalKey_b&lt;/code&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;If sequence equal, compare type&lt;/strong&gt; (descending order)&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;kTypeValue&lt;/code&gt; (0x1) &amp;lt; &lt;code&gt;kTypeDeletion&lt;/code&gt; (0x0)&lt;/li&gt;
&lt;li&gt;Values come before deletions&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;&lt;strong&gt;Why descending sequence?&lt;/strong&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;When searching for a key, we want to find the newest version first&lt;/li&gt;
&lt;li&gt;SkipList returns the first match, so newer versions must sort earlier&lt;/li&gt;
&lt;/ul&gt;
&lt;hr /&gt;
&lt;h2&gt;LookupKey Format&lt;/h2&gt;
&lt;p&gt;&lt;code&gt;LookupKey&lt;/code&gt; is used for querying in MemTable. It encodes the search target.&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;---
config:
  packet:
    bitsPerRow: 32
---
packet-beta
0-31: &quot;internal_key_len: varint32&quot;
32-255: &quot;user_key: bytes[user_key_len]&quot;
256-319: &quot;tag: uint64 (seq &amp;lt;&amp;lt; 8 | type)&quot;
&lt;/code&gt;&lt;/pre&gt;
&lt;pre&gt;&lt;code&gt;LookupKey Layout:
┌─────────────────┬──────────────────────┬─────────────────┐
│ internal_key_len│      user_key        │      tag        │
│   (varint32)    │   (user_key_len)     │    (uint64)     │
└─────────────────┴──────────────────────┴─────────────────┘
     ↑                 ↑                                    ↑
   start_           kstart_                               end_
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;strong&gt;Three key pointers:&lt;/strong&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;start_&lt;/code&gt;: Points to &lt;code&gt;internal_key_len&lt;/code&gt; (for MemTable queries)&lt;/li&gt;
&lt;li&gt;&lt;code&gt;kstart_&lt;/code&gt;: Points to &lt;code&gt;user_key&lt;/code&gt; (for InternalKey extraction)&lt;/li&gt;
&lt;li&gt;&lt;code&gt;end_&lt;/code&gt;: Points to end of tag&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;strong&gt;Usage:&lt;/strong&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;LookupKey lkey(user_key, sequence);
Slice memtable_key = lkey.memtable_key();  // [start_, end_)
Slice internal_key = lkey.internal_key();   // [kstart_, end_)
Slice user_key = lkey.user_key();           // [kstart_, end_ - 8)
&lt;/code&gt;&lt;/pre&gt;
&lt;hr /&gt;
&lt;h2&gt;Value Types&lt;/h2&gt;
&lt;pre&gt;&lt;code&gt;enum ValueType { 
    kTypeDeletion = 0x0,  // Tombstone marker
    kTypeValue = 0x1      // Actual value
};
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;strong&gt;Why store deletions?&lt;/strong&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Deletions are &lt;strong&gt;tombstone markers&lt;/strong&gt; in LSM-tree&lt;/li&gt;
&lt;li&gt;They mark keys as deleted without physically removing old versions&lt;/li&gt;
&lt;li&gt;Compaction will eventually remove tombstones and their older versions&lt;/li&gt;
&lt;/ul&gt;
&lt;hr /&gt;
&lt;h2&gt;Sequence Numbers&lt;/h2&gt;
&lt;pre&gt;&lt;code&gt;using SequenceNumber = uint64_t;
static const SequenceNumber kMaxSequenceNumber = ((0x1ull &amp;lt;&amp;lt; 56) - 1);
&lt;/code&gt;&lt;/pre&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Range&lt;/strong&gt;: 0 to 2^56 - 1 (leaves 8 bits for &lt;code&gt;ValueType&lt;/code&gt;)&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Purpose&lt;/strong&gt;: MVCC version control&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Monotonically increasing&lt;/strong&gt;: Each write operation increments the sequence&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Snapshot isolation&lt;/strong&gt;: Readers can specify a sequence to read consistent data&lt;/li&gt;
&lt;/ul&gt;
&lt;hr /&gt;
&lt;h2&gt;Comparator Design&lt;/h2&gt;
&lt;h3&gt;InternalKeyComparator&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;class InternalKeyComparator {
public:
    explicit InternalKeyComparator(const Comparator* user_cmp);
    
    // Compare two InternalKeys
    int Compare(const Slice&amp;amp; a, const Slice&amp;amp; b) const;
    
private:
    const Comparator* user_comparator_;
};
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;strong&gt;Comparison logic:&lt;/strong&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;int InternalKeyComparator::Compare(const Slice&amp;amp; a, const Slice&amp;amp; b) const {
    // 1. Extract user keys (strip last 8 bytes)
    Slice user_a = ExtractUserKey(a);
    Slice user_b = ExtractUserKey(b);
    
    // 2. Compare user keys (ascending)
    int r = user_comparator_-&amp;gt;Compare(user_a, user_b);
    if (r != 0) return r;
    
    // 3. Extract tags (last 8 bytes)
    uint64_t tag_a = DecodeFixed64(a.data() + a.size() - 8);
    uint64_t tag_b = DecodeFixed64(b.data() + b.size() - 8);
    
    // 4. Compare tags (descending - larger tag comes first)
    if (tag_a &amp;gt; tag_b) return -1;
    if (tag_a &amp;lt; tag_b) return +1;
    return 0;
}
&lt;/code&gt;&lt;/pre&gt;
&lt;hr /&gt;
&lt;h2&gt;Design Rationale&lt;/h2&gt;
&lt;h3&gt;Why InternalKey?&lt;/h3&gt;
&lt;ol&gt;
&lt;li&gt;&lt;strong&gt;MVCC Support&lt;/strong&gt;: Multiple versions of the same key can coexist&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Snapshot Reads&lt;/strong&gt;: Readers can see a consistent view at a specific sequence&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Efficient Updates&lt;/strong&gt;: No need to physically delete old versions immediately&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Conflict Resolution&lt;/strong&gt;: Newer writes naturally override older ones&lt;/li&gt;
&lt;/ol&gt;
&lt;h3&gt;Why Length-Prefixed Encoding?&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;MemTable stores &lt;code&gt;const char*&lt;/code&gt; pointers, not Key objects&lt;/li&gt;
&lt;li&gt;Length-prefixed format allows decoding without knowing the key size beforehand&lt;/li&gt;
&lt;li&gt;Efficient for variable-length keys&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;Memory Layout Considerations&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;Traditional approach (not used):
  Node { Key key; Value value; Node* next[]; }
  
LevelDB approach:
  Node { const char* entry; Node* next[]; }
  
  entry points to:
  ┌──────────────┬─────────┬─────┬───────────┬───────┐
  │ internal_key │ user_key│ tag │ value_len │ value │
  │     _len     │         │     │           │       │
  └──────────────┴─────────┴─────┴───────────┴───────┘
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;strong&gt;Benefits:&lt;/strong&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Single memory allocation for key+value+metadata&lt;/li&gt;
&lt;li&gt;Better cache locality&lt;/li&gt;
&lt;li&gt;Arena-friendly (no small object allocations)&lt;/li&gt;
&lt;li&gt;Pointer-sized node overhead&lt;/li&gt;
&lt;/ul&gt;
</content:encoded><category>LevelDB</category><category>Prism</category><category>KV</category></item><item><title>LevelDB SSTable</title><link>https://puji4810.github.io/2025/11/01/LevelDB-SSTable/</link><guid isPermaLink="true">https://puji4810.github.io/2025/11/01/LevelDB-SSTable/</guid><description>SSTable file layout, block structure, and encoding details used by LevelDB.</description><pubDate>Sat, 01 Nov 2025 00:00:00 GMT</pubDate><content:encoded>&lt;h1&gt;SSTable Format&lt;/h1&gt;
&lt;p&gt;SSTable (Sorted String Table) is the on-disk storage format for Prism. It stores sorted key-value pairs in an immutable, block-based structure optimized for both sequential scans and random lookups.&lt;/p&gt;

&lt;h2&gt;File Layout&lt;/h2&gt;
&lt;pre&gt;&lt;code&gt;graph TB
    subgraph SSTable File
        DB1[Data Block 1]
        DB2[Data Block 2]
        DBN[Data Block N]
        MB1[Meta Block 1]
        MBK[Meta Block K]
        MIB[MetaIndex Block]
        IB[Index Block]
        F[Footer - 48 bytes]
    end
    
    DB1 --&amp;gt; DB2
    DB2 --&amp;gt; DBN
    DBN --&amp;gt; MB1
    MB1 --&amp;gt; MBK
    MBK --&amp;gt; MIB
    MIB --&amp;gt; IB
    IB --&amp;gt; F
    
    style F fill:#FF6B6B,stroke:#333,stroke-width:2px,color:#000
    style IB fill:#4ECDC4,stroke:#333,stroke-width:2px,color:#000
    style MIB fill:#4ECDC4,stroke:#333,stroke-width:2px,color:#000
    style MB1 fill:#95E1D3,stroke:#333,stroke-width:2px,color:#000
    style MBK fill:#95E1D3,stroke:#333,stroke-width:2px,color:#000
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;strong&gt;Structure:&lt;/strong&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;&amp;lt;beginning_of_file&amp;gt;
[data block 1]
[data block 2]
...
[data block N]
[meta block 1]          (optional, e.g., filter block)
...
[meta block K]          (optional, e.g., stats block)
[metaindex block]
[index block]
[footer]                (fixed 48 bytes)
&amp;lt;end_of_file&amp;gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;hr /&gt;
&lt;h2&gt;BlockHandle&lt;/h2&gt;
&lt;p&gt;A &lt;code&gt;BlockHandle&lt;/code&gt; is a pointer to a block within the file:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;struct BlockHandle {
    uint64_t offset;    // varint64: byte offset in file
    uint64_t size;      // varint64: block content size (excludes trailer)
};
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;strong&gt;Encoding:&lt;/strong&gt; Two consecutive varint64 values.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Usage:&lt;/strong&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Index block entries point to data blocks&lt;/li&gt;
&lt;li&gt;MetaIndex entries point to meta blocks (e.g., filter)&lt;/li&gt;
&lt;li&gt;Footer contains handles for metaindex and index blocks&lt;/li&gt;
&lt;/ul&gt;
&lt;hr /&gt;
&lt;h2&gt;Data Block Format&lt;/h2&gt;
&lt;p&gt;Data blocks store sorted key-value pairs with prefix compression.&lt;/p&gt;
&lt;h3&gt;Block Structure&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;graph LR
    subgraph Data Block
        R1[Record 1&amp;lt;br/&amp;gt;shared=0]
        R2[Record 2&amp;lt;br/&amp;gt;shared=3]
        R3[Record 3&amp;lt;br/&amp;gt;shared=5]
        RN[Record N&amp;lt;br/&amp;gt;shared=4]
        RA[Restart Array&amp;lt;br/&amp;gt;4 bytes each]
        RC[Restart Count&amp;lt;br/&amp;gt;4 bytes]
    end
    
    R1 --&amp;gt; R2
    R2 --&amp;gt; R3
    R3 --&amp;gt; RN
    RN --&amp;gt; RA
    RA --&amp;gt; RC
    
    style R1 fill:#2ECC71,stroke:#333,color:#000
    style RA fill:#FF6B6B,stroke:#333,color:#000
    style RC fill:#FF6B6B,stroke:#333,color:#000
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;Entry Encoding&lt;/h3&gt;
&lt;p&gt;Each key-value entry is encoded as:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;Entry := shared       (varint32)    // bytes shared with previous key
       | non_shared   (varint32)    // bytes following shared prefix
       | value_length (varint32)    // length of value
       | key_delta    [non_shared]  // unshared part of key
       | value        [value_length]
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;strong&gt;Example:&lt;/strong&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;Keys: &quot;apple&quot;, &quot;apply&quot;, &quot;application&quot;

Entry 1: shared=0, non_shared=5, value_len=10
         key_delta=&quot;apple&quot;, value=&quot;&amp;lt;10 bytes&amp;gt;&quot;

Entry 2: shared=4, non_shared=1, value_len=8
         key_delta=&quot;y&quot;, value=&quot;&amp;lt;8 bytes&amp;gt;&quot;
         (reconstructed key = &quot;appl&quot; + &quot;y&quot; = &quot;apply&quot;)

Entry 3: shared=4, non_shared=7, value_len=12
         key_delta=&quot;ication&quot;, value=&quot;&amp;lt;12 bytes&amp;gt;&quot;
         (reconstructed key = &quot;appl&quot; + &quot;ication&quot; = &quot;application&quot;)
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;Restart Points&lt;/h3&gt;
&lt;p&gt;Every &lt;code&gt;block_restart_interval&lt;/code&gt; entries (default 16), a &lt;strong&gt;restart point&lt;/strong&gt; is created:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;shared = 0&lt;/code&gt; (full key stored)&lt;/li&gt;
&lt;li&gt;Offset recorded in restart array&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;strong&gt;Purpose:&lt;/strong&gt; Enable binary search within block without decoding all entries.&lt;/p&gt;
&lt;h3&gt;Block Trailer&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;Block Trailer := compression_type (1 byte)
               | crc32c           (4 bytes)
&lt;/code&gt;&lt;/pre&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Compression type:&lt;/strong&gt; &lt;code&gt;0x00&lt;/code&gt; = none, &lt;code&gt;0x01&lt;/code&gt; = Snappy&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;CRC32C:&lt;/strong&gt; Checksum of &lt;code&gt;block_contents + compression_type&lt;/code&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;hr /&gt;
&lt;h2&gt;Index Block Format&lt;/h2&gt;
&lt;p&gt;The index block maps keys to data block locations.&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;graph TB
    subgraph Index Block
        E1[&quot;Key: separator1&amp;lt;br/&amp;gt;Value: BlockHandle1&quot;]
        E2[&quot;Key: separator2&amp;lt;br/&amp;gt;Value: BlockHandle2&quot;]
        EN[&quot;Key: separatorN&amp;lt;br/&amp;gt;Value: BlockHandleN&quot;]
    end
    
    E1 --&amp;gt; E2
    E2 --&amp;gt; EN
    
    subgraph Corresponding Data Blocks
        DB1[Data Block 1]
        DB2[Data Block 2]
        DBN[Data Block N]
    end
    
    E1 -.-&amp;gt; DB1
    E2 -.-&amp;gt; DB2
    EN -.-&amp;gt; DBN
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;strong&gt;Index Entry:&lt;/strong&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;Key   := shortest separator between blocks
         (last_key_in_block[i] &amp;lt; separator &amp;lt;= first_key_in_block[i+1])
Value := BlockHandle (varint64 offset | varint64 size)
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;strong&gt;Separator Generation:&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;For block &lt;code&gt;i&lt;/code&gt;, the index key is:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;if (i &amp;lt; num_blocks - 1) {
    // Use shortest separator between this block&apos;s last key and next block&apos;s first key
    comparator-&amp;gt;FindShortestSeparator(&amp;amp;last_key_block_i, first_key_block_i+1);
} else {
    // Last block: use short successor of last key
    comparator-&amp;gt;FindShortSuccessor(&amp;amp;last_key_block_i);
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;strong&gt;Benefits:&lt;/strong&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Reduced index size (shorter keys)&lt;/li&gt;
&lt;li&gt;Maintains correctness (separator still routes lookups correctly)&lt;/li&gt;
&lt;/ul&gt;
&lt;hr /&gt;
&lt;h2&gt;Meta Blocks&lt;/h2&gt;
&lt;p&gt;Meta blocks store auxiliary data that helps optimize table operations. Unlike data blocks which contain actual key-value pairs, meta blocks contain metadata.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Common meta block types:&lt;/strong&gt;&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;&lt;strong&gt;Filter block&lt;/strong&gt; - Bloom filters for fast negative lookups&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Stats block&lt;/strong&gt; - Statistics about the table (future)&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Compression dictionary&lt;/strong&gt; - Shared dictionary for compression (future)&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;&lt;strong&gt;Properties:&lt;/strong&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Stored after all data blocks&lt;/li&gt;
&lt;li&gt;Each has a unique name (e.g., &lt;code&gt;&quot;filter.bloom&quot;&lt;/code&gt;, &lt;code&gt;&quot;stats&quot;&lt;/code&gt;)&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Mapped by MetaIndex block&lt;/strong&gt; - readers use MetaIndex to locate specific meta blocks&lt;/li&gt;
&lt;li&gt;Formatted using &lt;code&gt;BlockBuilder&lt;/code&gt; (can have restart points, block trailer)&lt;/li&gt;
&lt;li&gt;Optional (table valid without them)&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;strong&gt;Distinction from Index Block:&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;While &lt;strong&gt;Index block&lt;/strong&gt; maps data block separators to data blocks, &lt;strong&gt;Meta blocks + MetaIndex&lt;/strong&gt; work as a pair:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Meta blocks&lt;/strong&gt; = storage for metadata (e.g., filter data)&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;MetaIndex block&lt;/strong&gt; = index/directory of meta blocks&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Think of it as: Data blocks → indexed by → Index block, while Meta blocks → indexed by → MetaIndex block.&lt;/p&gt;
&lt;hr /&gt;
&lt;h2&gt;MetaIndex Block Format&lt;/h2&gt;
&lt;p&gt;The MetaIndex block maps meta block names to their locations:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;Entry: Key   = meta_block_name (e.g., &quot;filter.bloom&quot;, &quot;stats&quot;)
       Value = BlockHandle (varint64 offset | varint64 size)
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;strong&gt;Purpose:&lt;/strong&gt; Allow readers to locate specific meta blocks by name without scanning.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Example entries:&lt;/strong&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;Key=&quot;filter.bloom&quot; → Value=BlockHandle(offset=12345, size=4096)
Key=&quot;stats&quot;        → Value=BlockHandle(offset=16441, size=256)
Key=&quot;dict&quot;         → Value=BlockHandle(offset=16697, size=1024)
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;strong&gt;Format Characteristics:&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;MetaIndex blocks &lt;strong&gt;technically use the same BlockBuilder&lt;/strong&gt; as all other blocks (with restart points and trailer), but behave &lt;strong&gt;more like Index blocks&lt;/strong&gt; in practice:&lt;/p&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Characteristic&lt;/th&gt;
&lt;th&gt;Index Block&lt;/th&gt;
&lt;th&gt;MetaIndex Block&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Entry count&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Medium (10s-100s)&lt;/td&gt;
&lt;td&gt;Small (typically &amp;lt; 10)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Prefix compression&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Effective (data block separators share prefixes)&lt;/td&gt;
&lt;td&gt;Ineffective (meta block names have no common prefixes)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Restart points&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Multiple (one per 16 entries)&lt;/td&gt;
&lt;td&gt;Usually only 1 (at offset 0)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Search method&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Binary search&lt;/td&gt;
&lt;td&gt;Linear scan (enough for small count)&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p&gt;&lt;strong&gt;Typical structure:&lt;/strong&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;Entry 1: shared=0, key=&quot;dict&quot;,          value=BlockHandle(...)
Entry 2: shared=0, key=&quot;filter.bloom&quot;,  value=BlockHandle(...)
Entry 3: shared=0, key=&quot;stats&quot;,         value=BlockHandle(...)

Restart Array: [0]                      (only one restart point)
Restart Count: 1
Block Trailer: compression_type | crc32c
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;strong&gt;Why use BlockBuilder if it&apos;s mostly ineffective?&lt;/strong&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Code reuse:&lt;/strong&gt; Single block format for all block types&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Consistency:&lt;/strong&gt; Unified reading/verification logic for all blocks&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Future-proof:&lt;/strong&gt; If meta block names grow or share prefixes, compression becomes useful&lt;/li&gt;
&lt;/ul&gt;
&lt;hr /&gt;
&lt;h2&gt;Block Types Comparison&lt;/h2&gt;
&lt;p&gt;To clarify the three main block types and their relationships:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;graph TB
    subgraph Data_Layer[&quot;Data Layer&quot;]
        DB1[&quot;Data Block 1&amp;lt;br/&amp;gt;(KV pairs)&quot;]
        DB2[&quot;Data Block 2&amp;lt;br/&amp;gt;(KV pairs)&quot;]
        DBN[&quot;Data Block N&amp;lt;br/&amp;gt;(KV pairs)&quot;]
    end
    
    subgraph Index_Layer[&quot;Index Layer&quot;]
        IB[&quot;Index Block&amp;lt;br/&amp;gt;(separator → BlockHandle)&quot;]
    end
    
    subgraph Meta_Layer[&quot;Meta Layer&quot;]
        MB1[&quot;Filter Block&amp;lt;br/&amp;gt;(filter data)&quot;]
        MB2[&quot;Stats Block&amp;lt;br/&amp;gt;(statistics)&quot;]
        MIB[&quot;MetaIndex Block&amp;lt;br/&amp;gt;(name → BlockHandle)&quot;]
    end
    
    DB1 --&amp;gt; IB
    DB2 --&amp;gt; IB
    DBN --&amp;gt; IB
    
    MB1 --&amp;gt; MIB
    MB2 --&amp;gt; MIB
    
    style DB1 fill:#2ECC71,stroke:#333,stroke-width:2px,color:#000
    style DB2 fill:#2ECC71,stroke:#333,stroke-width:2px,color:#000
    style DBN fill:#2ECC71,stroke:#333,stroke-width:2px,color:#000
    style IB fill:#4ECDC4,stroke:#333,stroke-width:2px,color:#000
    style MB1 fill:#F39C12,stroke:#333,stroke-width:2px,color:#000
    style MB2 fill:#F39C12,stroke:#333,stroke-width:2px,color:#000
    style MIB fill:#FF6B6B,stroke:#333,stroke-width:2px,color:#000
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;Detailed Comparison&lt;/h3&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Aspect&lt;/th&gt;
&lt;th&gt;Data Block&lt;/th&gt;
&lt;th&gt;Index Block&lt;/th&gt;
&lt;th&gt;MetaIndex Block&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Purpose&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Store actual KV pairs&lt;/td&gt;
&lt;td&gt;Index data blocks&lt;/td&gt;
&lt;td&gt;Index meta blocks&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Indexed by&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Index block&lt;/td&gt;
&lt;td&gt;Footer&lt;/td&gt;
&lt;td&gt;Footer&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Entry content&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Key → Value&lt;/td&gt;
&lt;td&gt;Separator → BlockHandle&lt;/td&gt;
&lt;td&gt;Meta name → BlockHandle&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Entry count&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Large (100s-1000s)&lt;/td&gt;
&lt;td&gt;Medium (10s-100s)&lt;/td&gt;
&lt;td&gt;Small (typically &amp;lt; 10)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Prefix compression&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Very effective&lt;/td&gt;
&lt;td&gt;Effective&lt;/td&gt;
&lt;td&gt;Ineffective (names differ)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Restart interval&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Every 16 entries&lt;/td&gt;
&lt;td&gt;Every 16 entries&lt;/td&gt;
&lt;td&gt;Usually only 1 (all shared=0)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Must have&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Yes&lt;/td&gt;
&lt;td&gt;Yes&lt;/td&gt;
&lt;td&gt;Only if meta blocks exist&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Format type&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;BlockBuilder&lt;/code&gt; output&lt;/td&gt;
&lt;td&gt;&lt;code&gt;BlockBuilder&lt;/code&gt; output&lt;/td&gt;
&lt;td&gt;&lt;code&gt;BlockBuilder&lt;/code&gt; output&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Typical size&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;~4 KB each&lt;/td&gt;
&lt;td&gt;A few KB&lt;/td&gt;
&lt;td&gt;&amp;lt; 1 KB&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;h3&gt;Storage Hierarchy&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;SSTable File Structure:
├── Layer 1: Data
│   ├── Data Block 1 (4 KB)
│   ├── Data Block 2 (4 KB)
│   └── ...
├── Layer 2: Metadata
│   ├── Filter Block (2 KB)
│   ├── Stats Block (256 B)
│   └── ...
├── Layer 3: Indices
│   ├── MetaIndex Block (200 B) ← indexes Layer 2
│   └── Index Block (4 KB)      ← indexes Layer 1
└── Layer 4: Navigation
    └── Footer (48 B)           ← indexes Layer 3
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;Query Flow Examples&lt;/h3&gt;
&lt;p&gt;&lt;strong&gt;Example 1: Find data value&lt;/strong&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;Footer → Index Block location
       → Read Index Block
       → Binary search for key
       → Get Data Block location
       → Read Data Block
       → Seek/Linear scan for value
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;strong&gt;Example 2: Use filter block&lt;/strong&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;Footer → MetaIndex Block location
       → Read MetaIndex Block
       → Lookup &quot;filter.bloom&quot;
       → Get Filter Block location
       → Read Filter Block
       → Check if key might exist
       → If no → return NotFound (fast path)
       → If yes → proceed with Example 1
&lt;/code&gt;&lt;/pre&gt;
&lt;hr /&gt;
&lt;h2&gt;Filter Block Format&lt;/h2&gt;
&lt;p&gt;Stores Bloom filters for data blocks.&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;[filter 0]                            // For keys in data blocks [0*base, 1*base)
[filter 1]                            // For keys in data blocks [1*base, 2*base)
...
[filter N-1]

[offset of filter 0]                  // fixed32
[offset of filter 1]                  // fixed32
...
[offset of filter N-1]                // fixed32

[offset of offset array]              // fixed32
lg(base)                              // 1 byte (default: lg(2048) = 11)
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;strong&gt;Base:&lt;/strong&gt; 2KB (2048 bytes). All keys in data blocks whose file offset falls in &lt;code&gt;[i*2KB, (i+1)*2KB)&lt;/code&gt; are combined into filter &lt;code&gt;i&lt;/code&gt;.&lt;/p&gt;
&lt;hr /&gt;
&lt;h2&gt;Footer Format&lt;/h2&gt;
&lt;p&gt;The footer is &lt;strong&gt;fixed 48 bytes&lt;/strong&gt; at the end of the file:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;Footer := metaindex_handle (varint64 offset | varint64 size, padded to 20 bytes)
        | index_handle     (varint64 offset | varint64 size, padded to 20 bytes)
        | padding          (fill to 40 bytes total)
        | magic            (0xdb4775248b80fb57, fixed64, 8 bytes)
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;strong&gt;Total:&lt;/strong&gt; 40 bytes (handles + padding) + 8 bytes (magic) = 48 bytes.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Magic number:&lt;/strong&gt; &lt;code&gt;0xdb4775248b80fb57&lt;/code&gt; (little-endian)&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Reading:&lt;/strong&gt;&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;Seek to &lt;code&gt;file_size - 48&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;Read 48 bytes&lt;/li&gt;
&lt;li&gt;Verify magic number&lt;/li&gt;
&lt;li&gt;Decode &lt;code&gt;metaindex_handle&lt;/code&gt; and &lt;code&gt;index_handle&lt;/code&gt;&lt;/li&gt;
&lt;/ol&gt;
&lt;hr /&gt;
&lt;h2&gt;Building an SSTable&lt;/h2&gt;
&lt;pre&gt;&lt;code&gt;sequenceDiagram
    participant TB as TableBuilder
    participant DBB as DataBlockBuilder
    participant IBB as IndexBlockBuilder
    participant File as SSTable File

    loop For each sorted key-value pair
        TB-&amp;gt;&amp;gt;DBB: Add(key, value)
        alt Block size &amp;gt;= threshold
            DBB-&amp;gt;&amp;gt;TB: Finish() → block_contents
            TB-&amp;gt;&amp;gt;File: Write data block + trailer
            TB-&amp;gt;&amp;gt;IBB: Add(separator, BlockHandle)
            TB-&amp;gt;&amp;gt;DBB: Reset()
        end
    end
    
    TB-&amp;gt;&amp;gt;DBB: Finish() (final block)
    TB-&amp;gt;&amp;gt;File: Write final data block + trailer
    TB-&amp;gt;&amp;gt;IBB: Add(separator, BlockHandle)
    
    opt If filter policy
        TB-&amp;gt;&amp;gt;File: Write filter block + trailer
        TB-&amp;gt;&amp;gt;TB: Record filter BlockHandle
    end
    
    TB-&amp;gt;&amp;gt;File: Write metaindex block + trailer
    TB-&amp;gt;&amp;gt;IBB: Finish()
    TB-&amp;gt;&amp;gt;File: Write index block + trailer
    TB-&amp;gt;&amp;gt;File: Write footer (48 bytes)
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;strong&gt;Key steps:&lt;/strong&gt;&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;&lt;strong&gt;Add entries:&lt;/strong&gt; Call &lt;code&gt;TableBuilder::Add(key, value)&lt;/code&gt; in sorted order&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Flush data blocks:&lt;/strong&gt; When size threshold reached, flush to file&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Build index:&lt;/strong&gt; Record separator key → BlockHandle for each data block&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Write filter:&lt;/strong&gt; Optionally write Bloom filter&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Write metaindex:&lt;/strong&gt; Map &quot;filter.bloom&quot; → filter BlockHandle&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Write index:&lt;/strong&gt; Flush index block&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Write footer:&lt;/strong&gt; Record metaindex/index BlockHandles + magic&lt;/li&gt;
&lt;/ol&gt;
&lt;hr /&gt;
&lt;h2&gt;Reading from SSTable&lt;/h2&gt;
&lt;pre&gt;&lt;code&gt;sequenceDiagram
    participant C as Client
    participant T as Table
    participant IB as Index Block
    participant DB as Data Block
    participant Filter as Filter Block

    C-&amp;gt;&amp;gt;T: Open(filename)
    T-&amp;gt;&amp;gt;T: Read footer (last 48 bytes)
    T-&amp;gt;&amp;gt;T: Verify magic, extract handles
    T-&amp;gt;&amp;gt;T: Read index block
    T-&amp;gt;&amp;gt;C: Table ready

    C-&amp;gt;&amp;gt;T: InternalGet(key)
    T-&amp;gt;&amp;gt;IB: Seek(key)
    IB-&amp;gt;&amp;gt;T: BlockHandle of candidate data block
    
    opt If filter exists
        T-&amp;gt;&amp;gt;Filter: KeyMayMatch(key, block_id)
        alt Filter returns false
            T-&amp;gt;&amp;gt;C: NotFound (fast path)
        end
    end
    
    T-&amp;gt;&amp;gt;DB: ReadBlock(BlockHandle)
    DB-&amp;gt;&amp;gt;T: Block iterator
    T-&amp;gt;&amp;gt;DB: Seek(key) in block
    alt Key found
        DB-&amp;gt;&amp;gt;T: Value
        T-&amp;gt;&amp;gt;C: Value or NotFound (if kTypeDeletion)
    else Key not found
        T-&amp;gt;&amp;gt;C: NotFound
    end
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;strong&gt;Two-level lookup:&lt;/strong&gt;&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;&lt;strong&gt;Index block:&lt;/strong&gt; Binary search to find data block containing key&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Data block:&lt;/strong&gt; Binary search on restart points, then linear scan&lt;/li&gt;
&lt;/ol&gt;
&lt;hr /&gt;
&lt;h2&gt;Iterator Implementation&lt;/h2&gt;
&lt;h3&gt;Two-Level Iterator&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;graph TB
    subgraph TwoLevelIterator
        Index[Index Block Iterator&amp;lt;br/&amp;gt;outer]
        Data[Data Block Iterator&amp;lt;br/&amp;gt;inner]
    end
    
    Index --&amp;gt;|current entry&apos;s BlockHandle| Data
    
    subgraph Operations
        S[Seek/Next/Prev]
        S --&amp;gt; Index
        Index --&amp;gt; Data
    end
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;strong&gt;Algorithm:&lt;/strong&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;TwoLevelIterator:
    outer: index_block_iterator
    inner: data_block_iterator (lazy loaded)

    Seek(target):
        outer.Seek(target)
        InitInner()  // Read data block at outer.value()
        inner.Seek(target)

    Next():
        inner.Next()
        if !inner.Valid():
            outer.Next()
            if outer.Valid():
                InitInner()
                inner.SeekToFirst()

    key() := inner.key()
    value() := inner.value()
&lt;/code&gt;&lt;/pre&gt;
&lt;hr /&gt;
&lt;h2&gt;Compression&lt;/h2&gt;
&lt;p&gt;&lt;strong&gt;Supported types:&lt;/strong&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;kNoCompression (0x00)&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;kSnappyCompression (0x01)&lt;/code&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;strong&gt;Applied to:&lt;/strong&gt; Block contents (before appending trailer)&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Trade-off:&lt;/strong&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Pros:&lt;/strong&gt; Reduced disk I/O, storage space&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Cons:&lt;/strong&gt; CPU overhead on read/write&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;strong&gt;Default:&lt;/strong&gt; Snappy (fast compression/decompression)&lt;/p&gt;
&lt;hr /&gt;
&lt;h2&gt;CRC32C Checksum&lt;/h2&gt;
&lt;p&gt;&lt;strong&gt;Purpose:&lt;/strong&gt; Detect data corruption&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Coverage:&lt;/strong&gt; &lt;code&gt;block_contents + compression_type&lt;/code&gt;&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Algorithm:&lt;/strong&gt; CRC32C (Castagnoli polynomial)&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Location:&lt;/strong&gt; Last 4 bytes of block trailer&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Verification:&lt;/strong&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;uint32_t expected_crc = DecodeFixed32(trailer + 1);
uint32_t actual_crc = crc32c::Value(block_contents);
actual_crc = crc32c::Extend(actual_crc, &amp;amp;compression_type, 1);
if (actual_crc != expected_crc) {
    return Status::Corruption(&quot;Block checksum mismatch&quot;);
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;strong&gt;Note:&lt;/strong&gt; Unlike WAL, table block CRCs are &lt;strong&gt;not masked&lt;/strong&gt;.&lt;/p&gt;
&lt;hr /&gt;
&lt;h2&gt;InternalKey Encoding in SSTable&lt;/h2&gt;
&lt;p&gt;Keys stored in SSTable are &lt;strong&gt;InternalKeys&lt;/strong&gt;:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;InternalKey := user_key (variable length)
             | sequence_number (7 bytes, big-endian subset of tag)
             | value_type (1 byte: kTypeValue=1, kTypeDeletion=0)
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;strong&gt;Tag:&lt;/strong&gt; &lt;code&gt;(sequence &amp;lt;&amp;lt; 8) | type&lt;/code&gt;, stored as fixed64 (little-endian)&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Comparison order:&lt;/strong&gt;&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;User key: ascending (lexicographic)&lt;/li&gt;
&lt;li&gt;Sequence number: descending (newer first)&lt;/li&gt;
&lt;li&gt;Value type: descending (value before deletion)&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;&lt;strong&gt;Example:&lt;/strong&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;User puts: key=&quot;foo&quot;, value=&quot;v1&quot; at seq=10
User puts: key=&quot;foo&quot;, value=&quot;v2&quot; at seq=20
User deletes: key=&quot;foo&quot; at seq=30

SSTable stores (sorted by InternalKey):
  InternalKey(&quot;foo&quot;, 30, kTypeDeletion) → &quot;&quot; (empty value)
  InternalKey(&quot;foo&quot;, 20, kTypeValue) → &quot;v2&quot;
  InternalKey(&quot;foo&quot;, 10, kTypeValue) → &quot;v1&quot;

Get(&quot;foo&quot;, snapshot_seq=25) → &quot;v2&quot; (ignores seq=30)
Get(&quot;foo&quot;, snapshot_seq=35) → NotFound (sees deletion at seq=30)
&lt;/code&gt;&lt;/pre&gt;
&lt;hr /&gt;
&lt;h2&gt;Size Limits and Thresholds&lt;/h2&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Parameter&lt;/th&gt;
&lt;th&gt;Default&lt;/th&gt;
&lt;th&gt;Purpose&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;block_size&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;4 KB&lt;/td&gt;
&lt;td&gt;Target size for data blocks&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;block_restart_interval&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;16&lt;/td&gt;
&lt;td&gt;Restart points per block&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;write_buffer_size&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;4 MB&lt;/td&gt;
&lt;td&gt;MemTable flush threshold&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;filter_base&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;2 KB&lt;/td&gt;
&lt;td&gt;Filter granularity&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p&gt;&lt;strong&gt;Why 4KB blocks?&lt;/strong&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Matches typical filesystem/SSD page size&lt;/li&gt;
&lt;li&gt;Good balance: small enough for random reads, large enough for compression&lt;/li&gt;
&lt;/ul&gt;
&lt;hr /&gt;
&lt;h2&gt;TableCache (Future)&lt;/h2&gt;
&lt;pre&gt;&lt;code&gt;graph LR
    subgraph TableCache
        LRU[LRU Cache]
        H1[Handle: file_number=1 → Table*]
        H2[Handle: file_number=2 → Table*]
        HN[Handle: file_number=N → Table*]
    end
    
    LRU --&amp;gt; H1
    LRU --&amp;gt; H2
    LRU --&amp;gt; HN
    
    H1 --&amp;gt; T1[Table 1&amp;lt;br/&amp;gt;index in memory]
    H2 --&amp;gt; T2[Table 2&amp;lt;br/&amp;gt;index in memory]
    HN --&amp;gt; TN[Table N&amp;lt;br/&amp;gt;index in memory]
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;strong&gt;Purpose:&lt;/strong&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Keep recently-used tables open (file handles + index blocks)&lt;/li&gt;
&lt;li&gt;Avoid repeated file open/close overhead&lt;/li&gt;
&lt;li&gt;Amortize index block parsing&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;strong&gt;Eviction:&lt;/strong&gt; LRU (Least Recently Used)&lt;/p&gt;
&lt;hr /&gt;
&lt;h2&gt;Integration with DBImpl&lt;/h2&gt;
&lt;h3&gt;Minor Compaction (MemTable → SSTable)&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;sequenceDiagram
    participant DB as DBImpl
    participant Mem as MemTable
    participant Imm as Immutable MemTable
    participant TB as TableBuilder
    participant File as L0 SSTable

    DB-&amp;gt;&amp;gt;Mem: Write reaches threshold
    DB-&amp;gt;&amp;gt;DB: mem_ → imm_, create new mem_
    
    DB-&amp;gt;&amp;gt;Imm: NewIterator()
    Imm-&amp;gt;&amp;gt;DB: Iterator (sorted)
    
    loop For each entry
        DB-&amp;gt;&amp;gt;TB: Add(internal_key, value)
    end
    
    TB-&amp;gt;&amp;gt;File: Finish() → Write blocks, index, footer
    DB-&amp;gt;&amp;gt;DB: Update version (add new L0 file)
    DB-&amp;gt;&amp;gt;Imm: Unref() → delete
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;Read Path&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;graph TB
    Start[DBImpl::Get]
    Mem{Check mem_}
    Imm{Check imm_}
    L0{Check L0&amp;lt;br/&amp;gt;SSTables}
    L1{Check L1+&amp;lt;br/&amp;gt;SSTables}
    NotFound[Return NotFound]
    
    Start --&amp;gt; Mem
    Mem --&amp;gt;|Miss| Imm
    Mem --&amp;gt;|Hit| Return
    Imm --&amp;gt;|Miss| L0
    Imm --&amp;gt;|Hit| Return
    L0 --&amp;gt;|Miss| L1
    L0 --&amp;gt;|Hit| Return
    L1 --&amp;gt;|Miss| NotFound
    L1 --&amp;gt;|Hit| Return
    
    style Return fill:#2ECC71,stroke:#333,stroke-width:2px,color:#000
    style NotFound fill:#FF6B6B,stroke:#333,stroke-width:2px,color:#000
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;strong&gt;Search order:&lt;/strong&gt;&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;&lt;code&gt;mem_&lt;/code&gt; (current MemTable)&lt;/li&gt;
&lt;li&gt;&lt;code&gt;imm_&lt;/code&gt; (immutable MemTable being compacted)&lt;/li&gt;
&lt;li&gt;Level 0 SSTables (may overlap)&lt;/li&gt;
&lt;li&gt;Level 1+ SSTables (non-overlapping within level)&lt;/li&gt;
&lt;/ol&gt;
&lt;h2&gt;File Naming Convention&lt;/h2&gt;
&lt;pre&gt;&lt;code&gt;&amp;lt;dbname&amp;gt;/
  ├── CURRENT                  # Points to current MANIFEST
  ├── MANIFEST-000001          # Version metadata
  ├── 000003.log               # WAL (Write-Ahead Log)
  ├── 000005.sst               # SSTable files
  ├── 000007.sst
  └── LOG                      # Human-readable log
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;strong&gt;SSTable naming:&lt;/strong&gt; &lt;code&gt;&amp;lt;file_number&amp;gt;.sst&lt;/code&gt; or &lt;code&gt;&amp;lt;file_number&amp;gt;.ldb&lt;/code&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;File number: monotonically increasing uint64&lt;/li&gt;
&lt;li&gt;Managed by &lt;code&gt;VersionSet&lt;/code&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;References&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;&lt;a href=&quot;https://github.com/google/leveldb/blob/main/doc/table_format.md&quot;&gt;LevelDB Table Format&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://github.com/google/leveldb/blob/main/doc/impl.md&quot;&gt;LevelDB Implementation Notes&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://www.cs.umb.edu/~poneil/lsmtree.pdf&quot;&gt;LSM-Tree Paper&lt;/a&gt; - O&apos;Neil et al., 1996&lt;/li&gt;
&lt;/ul&gt;
&lt;hr /&gt;
&lt;p&gt;&lt;strong&gt;Last Updated:&lt;/strong&gt; 2025-01-16&lt;br /&gt;
&lt;strong&gt;Status:&lt;/strong&gt; Design documentation (implementation pending)&lt;/p&gt;
</content:encoded><category>LevelDB</category><category>Prism</category><category>KV</category></item><item><title>LevelDB Memformat</title><link>https://puji4810.github.io/2025/11/01/LevelDB-Memformat/</link><guid isPermaLink="true">https://puji4810.github.io/2025/11/01/LevelDB-Memformat/</guid><description>MemTable design: SkipList-based in-memory buffer, arena allocation, and encoded entry layout.</description><pubDate>Sat, 01 Nov 2025 00:00:00 GMT</pubDate><content:encoded>&lt;h1&gt;MemTable Design&lt;/h1&gt;
&lt;h2&gt;Overview&lt;/h2&gt;
&lt;p&gt;MemTable is an in-memory write buffer that:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Stores recent writes in sorted order&lt;/li&gt;
&lt;li&gt;Uses SkipList for efficient lookups and iteration&lt;/li&gt;
&lt;li&gt;Manages memory with Arena allocator&lt;/li&gt;
&lt;li&gt;Supports MVCC through InternalKey encoding&lt;/li&gt;
&lt;/ul&gt;

&lt;pre&gt;&lt;code&gt;graph TB
    subgraph MemTable[&quot;MemTable&quot;]
        A[&quot;Arena (memory pool)&quot;]
        B[&quot;SkipList&amp;amp;lt;const char*, KeyComparator&amp;amp;gt;&quot;]
        C[&quot;Reference counting&quot;]
        D[&quot;InternalKeyComparator&quot;]
    end
    
    E1[&quot;Entry 1: const char*&quot;]
    E2[&quot;Entry 2: const char*&quot;]
    E3[&quot;Entry N: const char*&quot;]
    
    D1[&quot;[encoded data in Arena]&quot;]
    D2[&quot;[encoded data in Arena]&quot;]
    D3[&quot;[encoded data in Arena]&quot;]
    
    MemTable --&amp;gt; E1
    MemTable --&amp;gt; E2
    MemTable --&amp;gt; E3
    
    E1 --&amp;gt; D1
    E2 --&amp;gt; D2
    E3 --&amp;gt; D3
    
    style MemTable fill:#4ECDC4,stroke:#333,stroke-width:2px
    style E1 fill:#95E1D3,stroke:#333
    style E2 fill:#95E1D3,stroke:#333
    style E3 fill:#95E1D3,stroke:#333
&lt;/code&gt;&lt;/pre&gt;
&lt;hr /&gt;
&lt;h2&gt;Entry Encoding Format&lt;/h2&gt;
&lt;p&gt;Each entry in MemTable is encoded as a single byte array allocated from Arena.&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;---
config:
  packet:
    bitsPerRow: 32
---
packet-beta
0-31: &quot;internal_key_len: varint32&quot;
32-255: &quot;user_key: bytes[user_key_len]&quot;
256-319: &quot;tag: uint64 (seq &amp;lt;&amp;lt; 8 | type)&quot;
320-351: &quot;value_len: varint32&quot;
352-511: &quot;value: bytes[value_len]&quot;
&lt;/code&gt;&lt;/pre&gt;
&lt;pre&gt;&lt;code&gt;Entry Layout (stored in Arena):
┌─────────────────┬──────────────┬─────┬─────────────┬──────────────┐
│ internal_key_len│  user_key    │ tag │ value_len   │   value      │
│   (varint32)    │(user_key_len)│(u64)│ (varint32)  │ (value_len)  │
└─────────────────┴──────────────┴─────┴─────────────┴──────────────┘
     ↑                                                       ↑
   buf (inserted into SkipList)                             end

where:
  internal_key_len = user_key_len + 8
  tag = (sequence &amp;lt;&amp;lt; 8) | type
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;strong&gt;Size calculation:&lt;/strong&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;size_t encoded_len = 
    VarintLength(internal_key_size) +  // 1-5 bytes
    internal_key_size +                 // user_key_len + 8
    VarintLength(value_size) +          // 1-5 bytes
    value_size;                         // value_len
&lt;/code&gt;&lt;/pre&gt;
&lt;hr /&gt;
&lt;h2&gt;MemTable::Add() - Insert Operation&lt;/h2&gt;
&lt;pre&gt;&lt;code&gt;void MemTable::Add(SequenceNumber seq, ValueType type, 
                   const Slice&amp;amp; key, const Slice&amp;amp; value)
{
    // Step 1: Calculate sizes
    size_t key_size = key.size();
    size_t val_size = value.size();
    size_t internal_key_size = key_size + 8;
    size_t encoded_len = VarintLength(internal_key_size) +
                         internal_key_size + 
                         VarintLength(val_size) + val_size;
  
    // Step 2: Allocate from Arena (single allocation)
    char* buf = arena_.Allocate(encoded_len);
  
    // Step 3: Encode entry
    char* p = EncodeVarint32(buf, internal_key_size);
    std::memcpy(p, key.data(), key_size);
    p += key_size;
    EncodeFixed64(p, (seq &amp;lt;&amp;lt; 8) | type);
    p += 8;
    p = EncodeVarint32(p, val_size);
    std::memcpy(p, value.data(), val_size);
  
    // Step 4: Insert pointer into SkipList
    table_.Insert(buf);
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;strong&gt;Example:&lt;/strong&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;Input:
  sequence = 100
  type = kTypeValue (0x1)
  key = &quot;foo&quot; (3 bytes)
  value = &quot;bar&quot; (3 bytes)

Encoded entry (hex):
  0B        // internal_key_len = 11 (varint32)
  66 6F 6F  // &quot;foo&quot;
  01 64 00 00 00 00 00 00  // tag = (100 &amp;lt;&amp;lt; 8) | 1
  03        // value_len = 3 (varint32)
  62 61 72  // &quot;bar&quot;
  
Total: 1 + 3 + 8 + 1 + 3 = 16 bytes
&lt;/code&gt;&lt;/pre&gt;
&lt;hr /&gt;
&lt;h2&gt;MemTable::Get() - Lookup Operation&lt;/h2&gt;
&lt;pre&gt;&lt;code&gt;bool MemTable::Get(const LookupKey&amp;amp; key, std::string* value, Status* s)
{
    // Step 1: Get memtable_key from LookupKey
    Slice memkey = key.memtable_key();
  
    // Step 2: Seek to first entry &amp;gt;= memkey
    Table::Iterator iter(&amp;amp;table_);
    iter.Seek(memkey.data());
  
    if (iter.Valid()) {
        const char* entry = iter.key();
      
        // Step 3: Parse entry and extract internal_key
        uint32_t key_length;
        const char* key_ptr = GetVarint32Ptr(entry, entry + 5, &amp;amp;key_length);
      
        // Step 4: Compare user_key
        if (comparator_.user_comparator()-&amp;gt;Compare(
                Slice(key_ptr, key_length - 8),  // Extract user_key
                key.user_key()) == 0) {
          
            // Step 5: Check type in tag
            uint64_t tag = DecodeFixed64(key_ptr + key_length - 8);
            ValueType type = static_cast&amp;lt;ValueType&amp;gt;(tag &amp;amp; 0xff);
          
            switch (type) {
                case kTypeValue: {
                    // Step 6: Extract value
                    Slice v = GetLengthPrefixedSlice(key_ptr + key_length);
                    value-&amp;gt;assign(v.data(), v.size());
                    return true;
                }
                case kTypeDeletion:
                    *s = Status::NotFound();
                    return true;
            }
        }
    }
    return false;  // Key not found
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;strong&gt;Search example:&lt;/strong&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;Query: user_key = &quot;foo&quot;, sequence = 200

LookupKey encoding:
  0B 66 6F 6F [C8 00 00 00 00 00 00 01]
  │  └─ &quot;foo&quot; └─ tag = (200 &amp;lt;&amp;lt; 8) | kTypeValue
  └─ internal_key_len = 11

SkipList has:
  Entry1: 0B 66 6F 6F [64 00 00 00 00 00 00 01] 03 62 61 72  (seq=100)
  Entry2: 0B 66 6F 6F [C8 00 00 00 00 00 00 01] 03 62 61 7A  (seq=200)
  Entry3: 0B 66 6F 6F [2C 01 00 00 00 00 00 01] 03 62 61 78  (seq=300)

Seek(LookupKey) → finds Entry2 (first entry &amp;gt;= search key)
Since InternalKeyComparator sorts by (user_key ASC, seq DESC),
Entry3 (seq=300) comes before Entry2 (seq=200)

Corrected SkipList order:
  Entry3: seq=300 (newest)
  Entry2: seq=200
  Entry1: seq=100 (oldest)

Seek with seq=200 → should find Entry2 or Entry3 depending on exact comparison
&lt;/code&gt;&lt;/pre&gt;
&lt;hr /&gt;
&lt;h2&gt;KeyComparator Design&lt;/h2&gt;
&lt;p&gt;MemTable&apos;s SkipList compares &lt;code&gt;const char*&lt;/code&gt; pointers, not InternalKey objects.&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;struct KeyComparator {
    const InternalKeyComparator comparator;
  
    explicit KeyComparator(const InternalKeyComparator&amp;amp; c) 
        : comparator(c) {}
  
    // Compare two entry pointers
    int operator()(const char* aptr, const char* bptr) const {
        // Extract InternalKey from length-prefixed encoding
        Slice a = GetLengthPrefixedSlice(aptr);
        Slice b = GetLengthPrefixedSlice(bptr);
      
        // Use InternalKeyComparator
        return comparator.Compare(a, b);
    }
};
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;strong&gt;How it works:&lt;/strong&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;aptr → [0B 66 6F 6F 64 00 00 00 00 00 00 01 03 62 61 72]
        └─ internal_key_len=11
                │
                v
       GetLengthPrefixedSlice
                │
                v
       Slice(&quot;foo\x64\x00\x00\x00\x00\x00\x00\x01&quot;, 11)
                │
                v
       InternalKeyComparator::Compare()
&lt;/code&gt;&lt;/pre&gt;
&lt;hr /&gt;
&lt;h2&gt;Reference Counting&lt;/h2&gt;
&lt;p&gt;MemTable uses manual reference counting to manage lifetime.&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;class MemTable {
public:
    explicit MemTable(const InternalKeyComparator&amp;amp; cmp)
        : comparator_(cmp), refs_(0), table_(comparator_, &amp;amp;arena_) {}
  
    void Ref() { ++refs_; }
  
    void Unref() {
        --refs_;
        assert(refs_ &amp;gt;= 0);
        if (refs_ &amp;lt;= 0) {
            delete this;  // Self-destruct
        }
    }
  
private:
    ~MemTable() { assert(refs_ == 0); }  // Private destructor
  
    int refs_;
    // ...
};
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;strong&gt;Usage pattern:&lt;/strong&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;// DBImpl holds MemTable
MemTable* mem_ = new MemTable(comparator);
mem_-&amp;gt;Ref();  // refs_ = 1

// When switching to immutable MemTable
MemTable* imm_ = mem_;
imm_-&amp;gt;Ref();  // refs_ = 2

mem_ = new MemTable(comparator);
mem_-&amp;gt;Ref();  // refs_ = 1

// After compaction completes
imm_-&amp;gt;Unref();  // refs_ = 1
imm_-&amp;gt;Unref();  // refs_ = 0 → delete this
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;strong&gt;Why reference counting?&lt;/strong&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Multiple components may hold MemTable (DBImpl, iterators, compaction)&lt;/li&gt;
&lt;li&gt;Allows safe concurrent reads during compaction&lt;/li&gt;
&lt;li&gt;Prevents premature deletion&lt;/li&gt;
&lt;/ul&gt;
&lt;hr /&gt;
&lt;h2&gt;Memory Management&lt;/h2&gt;
&lt;h3&gt;Arena Integration&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;class MemTable {
private:
    Arena arena_;
    Table table_;  // SkipList uses &amp;amp;arena_
};

// SkipList constructor
SkipList(Comparator cmp, Arena* arena)
    : arena_(arena), ... {}

// Node allocation
Node* SkipList::NewNode(const Key&amp;amp; key, int height) {
    char* mem = arena_-&amp;gt;AllocateAligned(
        sizeof(Node) + sizeof(atomic&amp;lt;Node*&amp;gt;) * (height - 1));
    return new (mem) Node(key);
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;strong&gt;Memory layout:&lt;/strong&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;Arena blocks:
┌──────────────────────────────────────────────┐
│ Block 1 (4KB)                                │
├──────────────────────────────────────────────┤
│ Entry1 [key+value data]                      │
│ SkipList Node1 [pointers]                    │
│ Entry2 [key+value data]                      │
│ SkipList Node2 [pointers]                    │
│ ...                                          │
└──────────────────────────────────────────────┘
┌──────────────────────────────────────────────┐
│ Block 2 (4KB)                                │
├──────────────────────────────────────────────┤
│ Entry N [key+value data]                     │
│ ...                                          │
└──────────────────────────────────────────────┘
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;strong&gt;Benefits:&lt;/strong&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;No per-object allocation overhead&lt;/li&gt;
&lt;li&gt;Better cache locality&lt;/li&gt;
&lt;li&gt;Fast allocation (bump-pointer in most cases)&lt;/li&gt;
&lt;li&gt;Bulk deallocation (free entire MemTable at once)&lt;/li&gt;
&lt;/ul&gt;
&lt;hr /&gt;
&lt;h2&gt;Iterator Design&lt;/h2&gt;
&lt;pre&gt;&lt;code&gt;class MemTableIterator : public Iterator {
public:
    explicit MemTableIterator(MemTable::Table* table) 
        : iter_(table) {}
  
    bool Valid() const override { return iter_.Valid(); }
    void Next() override { iter_.Next(); }
    void Prev() override { iter_.Prev(); }
    void SeekToFirst() override { iter_.SeekToFirst(); }
    void SeekToLast() override { iter_.SeekToLast(); }
  
    void Seek(const Slice&amp;amp; target) override {
        iter_.Seek(EncodeKey(&amp;amp;tmp_, target));
    }
  
    Slice key() const override {
        return GetLengthPrefixedSlice(iter_.key());
    }
  
    Slice value() const override {
        Slice key_slice = GetLengthPrefixedSlice(iter_.key());
        return GetLengthPrefixedSlice(key_slice.data() + key_slice.size());
    }
  
private:
    MemTable::Table::Iterator iter_;  // SkipList::Iterator
    std::string tmp_;  // Scratch space for encoding
};
&lt;/code&gt;&lt;/pre&gt;
&lt;hr /&gt;
&lt;h2&gt;Approximate Memory Usage&lt;/h2&gt;
&lt;pre&gt;&lt;code&gt;size_t MemTable::ApproximateMemoryUsage() {
    return arena_.MemoryUsage();
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;strong&gt;What&apos;s counted:&lt;/strong&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;All Arena blocks (entry data + SkipList nodes)&lt;/li&gt;
&lt;li&gt;Block overhead (&lt;code&gt;sizeof(char*)&lt;/code&gt; per block)&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;strong&gt;What&apos;s NOT counted:&lt;/strong&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;MemTable object itself (&lt;code&gt;sizeof(MemTable)&lt;/code&gt;)&lt;/li&gt;
&lt;li&gt;KeyComparator&lt;/li&gt;
&lt;li&gt;SkipList metadata (head node is in Arena)&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;strong&gt;Usage:&lt;/strong&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;// DBImpl decides when to create immutable MemTable
if (mem_-&amp;gt;ApproximateMemoryUsage() &amp;gt; options_.write_buffer_size) {
    // Switch to new MemTable
    imm_ = mem_;
    mem_ = new MemTable(comparator_);
    // Trigger compaction...
}
&lt;/code&gt;&lt;/pre&gt;
&lt;hr /&gt;
&lt;h2&gt;Design Decisions&lt;/h2&gt;
&lt;h3&gt;Why &lt;code&gt;const char*&lt;/code&gt; instead of Key objects?&lt;/h3&gt;
&lt;p&gt;&lt;strong&gt;Rejected design:&lt;/strong&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;using Table = SkipList&amp;lt;InternalKey, Comparator&amp;gt;;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;strong&gt;Problems:&lt;/strong&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Each node stores a copy of InternalKey&lt;/li&gt;
&lt;li&gt;Expensive key copies during insertion&lt;/li&gt;
&lt;li&gt;Separate allocation for value&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;strong&gt;Chosen design:&lt;/strong&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;using Table = SkipList&amp;lt;const char*, KeyComparator&amp;gt;;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;strong&gt;Benefits:&lt;/strong&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Single allocation for key + value + metadata&lt;/li&gt;
&lt;li&gt;Zero-copy insertion (just pointer)&lt;/li&gt;
&lt;li&gt;Arena-friendly (variable-size data)&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;Why length-prefixed encoding?&lt;/h3&gt;
&lt;p&gt;&lt;strong&gt;Alternative: Fixed struct&lt;/strong&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;struct Entry {
    uint32_t key_len;
    uint32_t value_len;
    char data[];
};
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;strong&gt;Problems:&lt;/strong&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Wastes space for small keys/values&lt;/li&gt;
&lt;li&gt;Alignment padding&lt;/li&gt;
&lt;li&gt;Harder to extend format&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;strong&gt;Length-prefixed (varint) benefits:&lt;/strong&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Compact (1 byte for lengths &amp;lt; 128)&lt;/li&gt;
&lt;li&gt;Self-describing&lt;/li&gt;
&lt;li&gt;Easy to parse forward&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;Why Arena for MemTable?&lt;/h3&gt;
&lt;p&gt;&lt;strong&gt;Benefits:&lt;/strong&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Fast allocation (no malloc overhead)&lt;/li&gt;
&lt;li&gt;No fragmentation&lt;/li&gt;
&lt;li&gt;Bulk deallocation&lt;/li&gt;
&lt;li&gt;Cache-friendly (sequential allocations)&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;strong&gt;Trade-offs:&lt;/strong&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Cannot delete individual entries&lt;/li&gt;
&lt;li&gt;Memory held until entire MemTable is freed&lt;/li&gt;
&lt;li&gt;OK for MemTable because it&apos;s immutable after becoming &lt;code&gt;imm_&lt;/code&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;hr /&gt;
&lt;h2&gt;Performance Characteristics&lt;/h2&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Operation&lt;/th&gt;
&lt;th&gt;Time Complexity&lt;/th&gt;
&lt;th&gt;Notes&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Add&lt;/td&gt;
&lt;td&gt;O(log N)&lt;/td&gt;
&lt;td&gt;SkipList insertion&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Get&lt;/td&gt;
&lt;td&gt;O(log N)&lt;/td&gt;
&lt;td&gt;SkipList search&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Iterator creation&lt;/td&gt;
&lt;td&gt;O(1)&lt;/td&gt;
&lt;td&gt;Lightweight wrapper&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Iterator advance&lt;/td&gt;
&lt;td&gt;O(1) amortized&lt;/td&gt;
&lt;td&gt;SkipList traversal&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Memory usage&lt;/td&gt;
&lt;td&gt;O(N)&lt;/td&gt;
&lt;td&gt;Linear in data size&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Destruction&lt;/td&gt;
&lt;td&gt;O(B)&lt;/td&gt;
&lt;td&gt;B = number of Arena blocks&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p&gt;&lt;strong&gt;Space overhead:&lt;/strong&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;Per entry:
  - Varint lengths: 2-10 bytes (typically 2)
  - Tag: 8 bytes
  - SkipList node: 16-32 bytes (depends on height)
  Total overhead: ~26-50 bytes per entry

For 1M entries of 100 bytes each:
  Data: 100 MB
  Overhead: ~26-50 MB
  Total: ~126-150 MB
&lt;/code&gt;&lt;/pre&gt;
</content:encoded><category>LevelDB</category><category>Prism</category><category>KV</category></item><item><title>Reading List 2025</title><link>https://puji4810.github.io/2025/10/28/Reading-lists/</link><guid isPermaLink="true">https://puji4810.github.io/2025/10/28/Reading-lists/</guid><description>Reading list and learning resources for 2025.</description><pubDate>Tue, 28 Oct 2025 00:00:00 GMT</pubDate><content:encoded>&lt;blockquote&gt;
&lt;p&gt;&quot;如果你发现你几乎把所有的时间都花在了理论上，那就开始把一些注意力转向实际的东西；这会提高你的理论水平。&quot; —— 高德纳&lt;/p&gt;
&lt;/blockquote&gt;
&lt;hr /&gt;
&lt;h2&gt;清单&lt;/h2&gt;
&lt;h3&gt;📚 Books（书籍）&lt;/h3&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;书名&lt;/th&gt;
&lt;th&gt;作者&lt;/th&gt;
&lt;th&gt;主题&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;a href=&quot;https://ddia.vonng.com/&quot;&gt;设计数据密集型应用（第二版）&lt;/a&gt;&lt;/td&gt;
&lt;td&gt;Martin Kleppmann&lt;/td&gt;
&lt;td&gt;分布式系统&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;a href=&quot;https://csapp.cs.cmu.edu/&quot;&gt;深入理解计算机系统 (CSAPP)&lt;/a&gt;&lt;/td&gt;
&lt;td&gt;Bryant &amp;amp; O&apos;Hallaron&lt;/td&gt;
&lt;td&gt;计算机基础&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;a href=&quot;https://github.com/munificent/craftinginterpreters&quot;&gt;Crafting Interpreters&lt;/a&gt;&lt;/td&gt;
&lt;td&gt;Robert Nystrom&lt;/td&gt;
&lt;td&gt;编程语言&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;hr /&gt;
&lt;h3&gt;Papers（论文/资源）&lt;/h3&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;题目&lt;/th&gt;
&lt;th&gt;来源&lt;/th&gt;
&lt;th&gt;主题&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;a href=&quot;https://aman.ai/primers/ai/top-30-papers/&quot;&gt;Ilya Sutskever&apos;s Top 30 Reading List&lt;/a&gt;&lt;/td&gt;
&lt;td&gt;&lt;a href=&quot;http://aman.ai/&quot;&gt;Aman.ai&lt;/a&gt;&lt;/td&gt;
&lt;td&gt;AI/ML&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;a href=&quot;https://pdos.csail.mit.edu/6.824/schedule.html&quot;&gt;MIT 6.824: Distributed Systems&lt;/a&gt;&lt;/td&gt;
&lt;td&gt;MIT&lt;/td&gt;
&lt;td&gt;分布式系统&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;a href=&quot;https://pdos.csail.mit.edu/6.828/2021/schedule.html&quot;&gt;MIT 6.081: Operating System Engineering&lt;/a&gt;&lt;/td&gt;
&lt;td&gt;MIT PDOS Lab&lt;/td&gt;
&lt;td&gt;操作系统&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;a href=&quot;https://docs.oracle.com/cd/E19957-01/806-3568/ncg_goldberg.html&quot;&gt;What Every Computer Scientist Should Know About Floating-Point Arithmetic&lt;/a&gt;&lt;/td&gt;
&lt;td&gt;David Goldberg&lt;/td&gt;
&lt;td&gt;数值计算&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;a href=&quot;https://research.swtch.com/mm&quot;&gt;Memory Models Series&lt;/a&gt;&lt;/td&gt;
&lt;td&gt;&lt;a href=&quot;https://swtch.com/~rsc/&quot;&gt;Russ Cox&lt;/a&gt;&lt;/td&gt;
&lt;td&gt;并发编程&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;a href=&quot;https://www.agner.org/optimize/optimizing_cpp.pdf&quot;&gt;Optimizing software in C++&lt;/a&gt;&lt;/td&gt;
&lt;td&gt;&lt;a href=&quot;https://www.agner.org/&quot;&gt;RAgner Fog&lt;/a&gt;&lt;/td&gt;
&lt;td&gt;程序优化&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;a href=&quot;https://en.algorithmica.org/hpc/&quot;&gt;Algorithms for Modern Hardware&lt;/a&gt;&lt;/td&gt;
&lt;td&gt;&lt;a href=&quot;http://sereja.me/&quot;&gt;Sergey Slotin&lt;/a&gt;&lt;/td&gt;
&lt;td&gt;程序优化&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;hr /&gt;
&lt;h3&gt;Blogs（博客文章）&lt;/h3&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;标题&lt;/th&gt;
&lt;th&gt;作者/来源&lt;/th&gt;
&lt;th&gt;主题&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;a href=&quot;https://xiaoweichen.github.io/Cpp-Memory-Management/&quot;&gt;C++ 内存管理&lt;/a&gt;&lt;/td&gt;
&lt;td&gt;&lt;a href=&quot;https://mysteriouspreserve.com/&quot;&gt;萧叶轩&lt;/a&gt;&lt;/td&gt;
&lt;td&gt;内存管理&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;a href=&quot;https://zhuanlan.zhihu.com/p/24861321599&quot;&gt;初级程序员也能快速深入理解 C++&lt;/a&gt;&lt;/td&gt;
&lt;td&gt;&lt;a href=&quot;https://www.zhihu.com/people/yu-guo-liang-22&quot;&gt;飘水&lt;/a&gt;&lt;/td&gt;
&lt;td&gt;C++ 基础&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;a href=&quot;https://lemire.me/blog/2025/06/22/c26-will-include-compile-time-reflection-why-should-you-care/&quot;&gt;C++26 Will Include Compile-Time Reflection&lt;/a&gt;&lt;/td&gt;
&lt;td&gt;Daniel Lemire&lt;/td&gt;
&lt;td&gt;C++ 新特性&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;a href=&quot;https://lemire.me/blog/2024/08/13/reflection-based-json-in-c-at-gigabytes-per-second/&quot;&gt;Reflection-Based JSON in C++ at Gigabytes per Second&lt;/a&gt;&lt;/td&gt;
&lt;td&gt;Daniel Lemire&lt;/td&gt;
&lt;td&gt;C++ 优化&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;a href=&quot;https://go.dev/ref/mem#model&quot;&gt;The Go Memory Model&lt;/a&gt;&lt;/td&gt;
&lt;td&gt;Go 官方&lt;/td&gt;
&lt;td&gt;内存模型&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;a href=&quot;https://mysteriouspreserve.com/blog/2023/04/19/Hardware-Memory-Model-zh/&quot;&gt;Hardware Memory Model (中文)&lt;/a&gt;&lt;/td&gt;
&lt;td&gt;&lt;a href=&quot;https://mysteriouspreserve.com/&quot;&gt;萧叶轩&lt;/a&gt;&lt;/td&gt;
&lt;td&gt;硬件内存&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;a href=&quot;https://mysteriouspreserve.com/blog/2023/04/19/Programing-Memory-Models-ch/&quot;&gt;Programming Memory Models (中文)&lt;/a&gt;&lt;/td&gt;
&lt;td&gt;&lt;a href=&quot;https://mysteriouspreserve.com/&quot;&gt;萧叶轩&lt;/a&gt;&lt;/td&gt;
&lt;td&gt;编程模型&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;a href=&quot;https://mysteriouspreserve.com/blog/2024/10/07/Cpp-Coroutine-Part-1/&quot;&gt;C++ Coroutine 系列&lt;/a&gt;&lt;/td&gt;
&lt;td&gt;&lt;a href=&quot;https://mysteriouspreserve.com/&quot;&gt;萧叶轩&lt;/a&gt;&lt;/td&gt;
&lt;td&gt;C++ 协程&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;a href=&quot;https://lewissbaker.github.io/&quot;&gt;Coroutine Theory &amp;amp; Practice&lt;/a&gt;&lt;/td&gt;
&lt;td&gt;Lewis Baker&lt;/td&gt;
&lt;td&gt;C++ 协程&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;a href=&quot;https://nigeltao.github.io/blog/2020/jsonptr.html&quot;&gt;Jsonptr: Memory-Safe, Zero-Allocation JSON Decoder&lt;/a&gt;&lt;/td&gt;
&lt;td&gt;Nigel Tao&lt;/td&gt;
&lt;td&gt;JSON 解析&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;a href=&quot;https://github.com/facebook/rocksdb/wiki/RocksDB-Overview&quot;&gt;RocksDB Architecture Guide&lt;/a&gt;&lt;/td&gt;
&lt;td&gt;RocksDB&lt;/td&gt;
&lt;td&gt;架构设计&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;a href=&quot;https://docs.oracle.com/cd/E19957-01/806-3568/ncgTOC.html&quot;&gt;Numerical Computation Guide&lt;/a&gt;&lt;/td&gt;
&lt;td&gt;Oracle&lt;/td&gt;
&lt;td&gt;数值计算&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;a href=&quot;https://abseil.io/fast/hints.html#the-importance-of-thinking-about-performance&quot;&gt;Performance Hints&lt;/a&gt;&lt;/td&gt;
&lt;td&gt;Abseil&lt;/td&gt;
&lt;td&gt;性能优化&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;hr /&gt;
&lt;p&gt;&lt;strong&gt;持续更新中... 最后更新: 2025-12-20&lt;/strong&gt;&lt;/p&gt;
</content:encoded><category>Reading</category><category>Life</category></item></channel></rss>