关注了就能看到更多这么棒的文章哦~
Exposing concurrency bugs with a custom scheduler
By Daroc Alden
February 5, 2025
FOSDEM
Gemini-1.5-flash translation
https://lwn.net/Articles/1007689/
Jake Hillion 在 FOSDEM 上做了一个关于使用 sched_ext 的演讲。sched_ext 是在 kernel 6.12 版本中引入的 BPF 调度框架,它可以帮助发现难以捉摸的并发问题。Jake Hillion 与 Johannes Bechberger 合作构建了一个调度器,可以在几分钟内揭示测试代码中理论上可能出现但未被观察到的并发错误。由于他们的调度器仅依赖于主线 (mainline) 内核功能,因此理论上它可以应用于任何在 Linux 上运行的应用程序——尽管由于该项目仍处于早期阶段,因此存在许多注意事项。
Johannes Bechberger 是一位 OpenJDK 开发人员,但不幸的是他未能参加本次演讲。由于 Java 有其自身的并发模型,而 OpenJDK 负责维护该模型,因此 Johannes Bechberger 经常需要花费时间来调试棘手的并发问题。在解决一个这样的 bug 之后,他花费了大量时间试图重现它,于是他提出了一个想法,即创建一个故意“错误地”调度进程的调度器,以便尝试使错误行为更有可能发生,从而更容易调试。
Jake Hillion 在 Meta 工作,主要从事自定义 Linux 调度器方面的工作。他说,Meta 在生产环境中使用 sched_ext 调度器,该调度器经过精心设计,针对他们的特定应用程序进行了调整,并且非常可靠。而 Jake Hillion 和 Johannes Bechberger 构建的 concurrency-fuzz-scheduler
则恰恰相反。
一个糟糕的调度器
为了说明调度器如何暴露应用程序中的 bug,Jake Hillion 举了一个极其简化的例子,说明某些应用程序具有的工作负载类型:一个生产线程产生工作项,而一个消费线程消费它们。这些项使用队列在线程之间传递。但是,每个项都有一个过期时间,超过该时间后尝试处理它会导致错误。
这个模型可能看起来过于简单,但它描述了 Jake Hillion 曾经参与过的应用程序。他特别提到了一个大型 C++ 应用程序的案例,其中有人复制了指向某些共享数据的指针,而不是复制数据,并且没有正确验证数据是否能保持足够长的存活时间。在该应用程序中,只要使用复制指针的线程被足够频繁地调度,它就会在数据被删除之前运行,并且一切似乎都能正常工作。但是当系统承受 heavy load(重负载)时,处理线程无法跟上,并且它开始访问已释放的内存。
Jake Hillion 和 Johannes Bechberger 的调度器会故意这样做:当一个线程准备好运行时,他们的调度器不会立即将其分配给 CPU,而是会根据一些可配置的参数将其置于睡眠状态一段随机的时间。这使得通常不会有任何问题来相互配合的线程有机会让一个线程 race ahead(争先)并压倒另一个线程。它还可以确保线程以随机顺序运行,这也有助于暴露 bug。Jake Hillion 解释说,在某种程度上,正是因为 Linux 的默认调度器非常好,才使得这些问题难以重现。EEVDF 太过公平,并且只有在机器已经过载时才会真正让一个线程压倒另一个线程,即使那样也很少发生。
以随机顺序运行线程的想法并不新鲜。有很多专门的并发测试工具可以做类似的事情。但是 concurrency-fuzz-scheduler
远没有这些工具那么精细。例如,它不能帮助确定性地发现特定内存模型的违规行为。但是,它可以发现现实应用程序中常见的那些 gross logic errors(严重的逻辑错误)。
Jake Hillion 展示了一个 Java 应用程序的录像,该应用程序实现了他的示例,并在受调度器管理的情况下运行。在不到一分钟的时间内,该应用程序崩溃了——这是一个他们无法在该机器上重现的事件,即使没有调度器的情况下让他们运行好几天也无法重现。
早期阶段
最终,Jake Hillion 希望 concurrency-fuzz-scheduler
能够在生产机器上运行,从而提高发现 bug 的概率,而又不会对工作负载的性能产生太大的影响。但是,就目前而言,使用该调度器运行的任务最终运行速度会比原本慢很多倍,这是因为增加了延迟。因此,目前该调度器更适合在 individual developer's workstation(个人开发工作站)上使用,以便尝试重现间歇性 bug。
不过,Jake Hillion 期望通过更多的工作来解决这个缺点。他在 Meta 的工作经历告诉他,使用 sched_ext 为大型机器编写高性能的调度器有点棘手。最简单的调度器(他在演讲中展示了代码)实际上非常容易实现:新创建的可运行任务被添加到 global queue(全局队列)中。当 CPU 即将进入 idle(空闲)状态时,它会从 global queue(全局队列)中拉取一个任务。
当有人就该主题发表演讲时,这种简单的 sched_ext 演示几乎是必不可少的; scx repository 中的简单 C 和 Rust 示例已被展示过很多次。但是,Jake Hillion 的示例代码是用 Java 编写的—— concurrency-fuzz-scheduler
也是如此。Johannes Bechberger 的 other projects 一直在努力使将 Java 编译为 BPF bytecode 以供内核使用成为可能。不幸的是,Jake Hillion 在这方面的工作做得不多,并且没有花太多时间谈论它。
无论如何,已被用于演示该项目的简单 sched_ext 示例虽然对于小型系统来说是安全且可用的,但无法扩展到更大的系统。每当不同的 CPU 需要切换任务时,使用单个 global queue(全局队列)会在不同的 CPU 之间产生 synchronization costs(同步成本)。对于具有数十个 CPU 的系统,尤其是在单独的 sockets(插槽)上,这些 synchronization costs(同步成本)有时会变得非常糟糕,以至于系统实际上变得足够慢,从而触发内核的 softlockup detector 并强制重启,Jake Hillion 说。
他解释说,sched_ext 有一个 watchdog(看门狗),它应该踢出行为不端的调度器并切换回 EEVDF。但是,如果一台足够大的机器上的调度器依赖于快速更新 global state(全局状态),则在不同 cores(核心)之间同步内存的 hardware cost(硬件成本)实际上可能会使事情延迟到系统无法在 softlockup detector(软锁检测器)跳闸之前恢复。实际上,这不是什么大问题。只有当大型机器的所有 cores(核心)都在不断争用相同的 global queue(全局队列)时,事情才会变得一团糟到足以引起问题;给每个 core(核心)一个 local queue(本地队列),并批量传输任务或在没有足够任务时让 cores(核心)进入睡眠状态可以完全避免该问题。
然而, concurrency-fuzz-scheduler
最终会 故意 在其调度的任务中引入严重的延迟,通常一次只运行一个线程,而让许多 CPU 处于 idle(空闲)状态。然后,当它确实运行另一个线程时,它很可能会在另一个 CPU 上运行,以尝试暴露 synchronization problems(同步问题)。没有表现良好的调度器通常会如此多地让 CPU 处于 idle(空闲)状态,或者,如果它这样做了,它至少会让它们进入睡眠状态,而不是让它们不断地争用调度器的 global data structures(全局数据结构)。 deliberate delays(有意的延迟)和 cross-CPU communication(跨 CPU 通信)加在一起可能会导致 concurrency-fuzz-scheduler
超时并在更大的系统上触发 softlockup detector(软锁检测器)。因此,虽然该调度器完全适合在程序员的计算机上使用,但它尚未达到可以部署到生产环境(甚至部署到足够大的测试系统)的状态。
Jake Hillion 说,该项目的后续步骤是使其更加可靠——将其扩展到更大的机器,尝试提出以更少的性能开销来引发尽可能多问题的方法,并为 deterministic randomness(确定性随机性)添加一个 seed(种子)。
问题
在演讲结束后,一位听众询问该调度器是否实现了一些 intelligent(智能)的事情,例如查看应用程序的内存以决定要调度哪些线程。Jake Hillion 说,目前还没有,但是他想研究启用应用程序和调度器之间更多通信的可能性。Meta 的生产调度器允许应用程序向调度器提供有关应如何运行它们的提示,并且 concurrency-fuzz-scheduler
可能会做同样的事情。应该可以做一些事情,例如专门在任务处于 critical section(临界区)或持有 lock(锁)时 preempt(抢占)它——只是尚未实现。
另一个人询问了 erratic behavior(不稳定行为)的其他来源。例如,模拟额外的 memory latency(内存延迟)。Jake Hillion 同意这将有助于捕获其他 bug,但是他说,干扰进程比仅仅编写一个调度器要困难得多。他说,你可以做一些事情,例如将任务切换到另一个 CPU core(核心)以强制其退出 cache(缓存),但这几乎就是全部了。调度器需要做很多额外的工作才能实际干扰任务的运行,而不是仅仅在任务的时间片到期时才进行干扰。
在回答了几个相关问题之后,Jake Hillion 说,他很高兴看到这么多人有有趣的 scheduling-related ideas(调度相关的想法)。他专业地从事调度器方面的工作,而 Johannes Bechberger 也对调度器很感兴趣。但是,sched_ext 的部分承诺是,它将向更多人开放调度器的开发,并且他很高兴看到这是否意味着那些没有深入参与该领域的人实际上可以尝试其中的一些想法。
Jake Hillion 和 Johannes Bechberger 的 concurrency-fuzz-scheduler
是 sched_ext 调度器的一个有希望的例子,它不仅仅充当调度想法的试验台。它是一种实用的工具,可以从应用程序中 shake more bugs out(挖掘出更多 bug)——在一个软件只会变得越来越复杂的世界中,这总是有帮助的。
[不幸的是,LWN 今年无法派人亲自参加 FOSDEM。但是,感谢这次演讲的精彩 video,我们得以报道此事。]
全文完
LWN 文章遵循 CC BY-SA 4.0 许可协议。
欢迎分享、转载及基于现有协议再创作~
长按下面二维码关注,关注 LWN 深度文章以及开源社区的各种新近言论~