.NET 线程池是一项非常了不起的技术,适用范围很广。 RavenDB 从一开始就将它用于几乎所有并发工作。
在 RavenDB 3.5 中,我们决定改变这一点。 RavenDB 有很多并行执行需求,但其中大部分都有独特的特性,我们可以用自己的线程池更好地表达。
首先,与普通线程池不同,我们不只是注册一个委托和一些状态供其执行,我们总是注册一个要处理的项目列表,以及一个从该列表中获取单个项目或该列表的一部分。这让我们在工作窃取方面做得更好。因为我们对实际操作有更多的了解。我们 知道 当我们执行完一个特定的委托时,我们可以在列表中的下一个可用项目上运行相同的委托。这给了我们更高的代码局部性,因为我们总是执行相同的任务,只要我们在池中有任务。
我们经常有嵌套操作,一个并行任务(执行索引工作)会产生额外的并行工作(索引以下文档)。通过将这一切都基于我们的自定义线程池,我们可以以一种不涉及等待该工作完成的方式执行这些操作。相反,我们运行的线程池线程能够通过执行我们正在等待的工作来“等待”。我们没有阻塞的线程,并且在许多情况下我们可以避免任何上下文切换。
在负载下,这意味着线程不会将大量工作放在线程池上,然后必须相互争夺谁先完成工作,这意味着我们可以运行自己的任务,并且只有当有是否有足够的线程可用,换句话说,我们是否会扩展更多线程。
说到负载,新的线程池还具有动态负载平衡功能。因为我们知道 RavenDB 只会将线程池用于后台工作,所以我们可以相应地确定事情的优先级。默认情况下,RavenDB 试图将 CPU 使用率保持在 60% – 80% 的范围内。如果我们检测到我们有更高的 CPU 使用率,我们将开始减少我们正在做的后台工作,以确保我们不会影响前排工作(如服务请求)。我们将通过更改后台线程的优先级来开始这样做,并最终停止处理大多数后台线程中的工作(当然,我们总是有最小数量的线程将继续工作)。
线程池可以做的另一件有趣的事情是检测和处理 slowpokes。一个常见的例子是需要很 长时间 才能运行的索引。明显超过所有其他指标。线程池可以释放所有其他索引,并让调用代码知道这个特定任务已被留给自己运行。然后 RavenDB 将拆分索引工作,因此慢速索引不会减慢所有其余索引。
将执行请求处理的前排工作(标准 .NET 线程池)和后台池(这是我们自己的自定义实现)之间的线程池分开,我们在环境中获得了更多的可预测性。我们不必担心索引作业会接管处理请求所需的线程,或者服务器上的请求会影响新数据库的加载等。
最后,就像现在 RavenDB 中的所有其他功能一样,我们有一组丰富的调试端点,可以详细地告诉我们到底发生了什么。当我们谈论运行数月和数年的系统时,或者当我们试图对有问题的服务器进行故障排除时,这一点至关重要。