我们如何在 Manticore Search 中重构系统线程

最近我们重构了我们的系统线程。由于我们从 Sphinx 分叉,早期的 searchd 所有系统任务都是以一种经常唤醒的方式进行的。每个服务在一个专用线程中工作,每 50 毫秒唤醒一次,检查是否有事情要做。这意味着即使是空闲的守护进程每秒也会唤醒 200 次“只是检查”一个任务。拥有 6 个这样的任务将使每秒唤醒 1200 次,这在使用 CPU 计数的 Amazon AWS 客户端中变得尤为明显。这些内部任务包括:

  • 普通索引轮换
  • ping 一个代理
  • 索引预加载
  • 刷新索引属性
  • 刷新实时索引
  • 刷新二进制日志

它们都是相当少见的(例如,刷新实时索引可能每 10 小时发生一次),因此每秒检查 200 次只是浪费 CPU。此外,所有这些任务本质上并不稳固(例如,向一个巨大的日志追加文本),而只是周期性动作(定期重复同一个任务)。考虑到这种动作方式,我们完全重写了整个内容:

  1. 首先,我们添加了一个线程来服务定时器,其中每个动作都被调度。
  2. 其次,我们添加了线程池来执行实际的动作。
  3. 因此,最终一个服务动作(任务)被调度,当定时器到达时,它被移到线程池中,然后执行。
  4. 完成后它被删除,因此周期性任务在结束时会重新调度自己并产生全新的任务。

通过这种方法,没有一堆专用的服务线程,只有一个定时器线程。而且,它不会每 20 毫秒唤醒,而是有一个带超时的二进制堆,仅在队列中最早的定时器指定的周期内唤醒。线程池可以并行运行最多 32 个线程,但实际上只有一个,有时两个在工作。每个线程都有预定义的空闲期(10 分钟),在此之后它将被结束,因此在“没有任务”的情况下,什么都不会空闲运行(即使定时器线程也是以“懒惰”的方式初始化的,即仅在实际任务需要调度时才启动)。在非常少见的任务(>10 分钟的周期)情况下,工作线程池也会被放弃,因此工作线程仅在有事情要做时才会被创建。因此,所有服务线程都被移除,不再每秒唤醒 CPU 数百次。

这种方法最有前景的变化是“ping”任务的新行为。在过去,我们收集了所有代理主机,并且每个 ping 间隔向它们发出“ping”命令作为一组。因此,如果一个主机很慢,整个组也会很慢。此外,这与实际主机状态无关——例如,当你通过查询加载一个主机时,不必单独 ping 它,因为查询本身提供了关于主机状态的全面统计信息。现在 ping 是具体的:它为每个主机单独计划,并且与每个主机的实际 last_answer_time 绑定。如果主机很慢——只有它的 ping 任务会等待,其他的会正常工作。如果主机负载过重——它的 last_answer_time 将单调更新,因此如果自 last_query_time 和 ping_interval 以来已经发生了一些查询,则不会发生实际的 ping。

现在的另一个特性是任务可以并行工作。假设,索引刷新可以同时对任意数量的索引执行,而不是串行,而是并行。目前,这个数字设置为 2 个作业,但这只是调优的问题。此外,当调度多个相似任务时,现在我们可以限制它们的数量。比如,“malloc_trim”没有必要调度超过一次,因此它是一种“单例”——如果一个被调度,另一个调度尝试将被丢弃。

通过这种任务管理添加的下一个特性源于现在所有任务都在一个队列中调度/计划(而不是不同的线程组),我们确切知道它将何时运行。因此,现在可以显示这样的统计信息,并通过添加“debug sched”、“debug tasks”和“debug systhreads”来完成。

第一个显示定时器的二进制堆:最上面的值表示下一个超时和与之相关的任务;其他值接下来显示(然而它们现在作为原始二进制堆显示;如果有需要,可以进行排序)。

“debug tasks”显示守护进程中注册的所有任务,以及它们的统计信息和属性(可以并行运行多少这样的任务;可以调度多少;当前执行多少,花费多少时间在一个任务上,最后一次完成的时间,执行了多少次,因队列过载而丢弃了多少,以及现在排队的有多少)。

最后,“debug systhreads”显示工作线程的状态,如内部 ID、线程 ID、最后运行时间、总 CPU 时间、总滴答数和完成的作业数量、最后一个作业花费的时间,以及工作线程空闲了多久。如前所述,空闲 10 分钟会导致工作线程停止。

升级到 Manticore 3.1.0 或更高版本 以受益于此更改

安装Manticore Search

安装Manticore Search