最近我们重新设计了系统线程。由于我们是从 Sphinx 分支出来的,早期的 searchd 所有系统任务都是以经常唤醒的方式实现的。每个服务都在一个专用线程中运行,每隔 50 毫秒就会唤醒一次,检查是否有任务需要执行。这意味着即使是一个空闲的守护进程也会每秒唤醒 200 次“只是为了检查”任务。如果有 6 个这样的任务,每秒就会唤醒 1200 次,这在 Amazon AWS 上的客户特别明显,因为 AWS 会计算 CPU 使用率。这些内部任务包括:
- 普通索引轮换
- 向代理发送心跳
- 索引预加载
- 刷新索引属性
- 刷新实时索引
- 刷新二进制日志
所有这些任务都相当罕见(例如刷新实时索引可能每 10 小时发生一次),所以每秒检查 200 次只是浪费 CPU 资源。此外,这些任务本质上并不是连续的(例如向大日志追加文本),而是周期性动作(定期重复执行相同任务)。考虑到这种动作方式,我们完全重写了整个系统:
- 首先,我们添加了一个线程来处理定时器,每个动作都会被安排。
- 其次,我们添加了线程池来执行这些动作本身。
- 这样,最终一个服务动作(任务)会被安排,当定时器触发时,它会被移动到线程池中,然后执行。
- 执行完成后会被删除,因此周期性任务只需在结束时重新安排自己,生成全新的任务。
采用这种方案后,不再有大量专用服务线程,只有一个定时器线程。而这个线程不会每 20 毫秒唤醒一次,而是使用一个二叉堆来管理超时,仅在队列中最早定时器指定的时间段内唤醒。线程池可以并行运行最多 32 个线程,但实际上通常只有 1 个,有时 2 个在运行。每个线程都有预定义的空闲时间(10 分钟),之后线程会终止,因此在没有任务的情况下,完全不会运行任何空闲线程(甚至定时器线程也是以“懒惰”方式初始化,即只有在实际需要安排任务时才会启动)。对于非常罕见(超过 10 分钟周期)的任务,工作线程池也会被丢弃,因此只有在有任务需要执行时才会创建工作线程。因此,所有服务线程都被移除,不会再每秒数百次地唤醒 CPU。
这种方案最显著的变化是“ping”任务的新行为。过去,我们收集所有代理主机,并在每次 ping 间隔时一次性向它们发送“ping”命令。因此,如果某台主机响应缓慢,整个批次都会变慢。此外,这与主机的实际状态无关——例如,当通过查询加载主机时,无需单独 ping 它,因为查询本身已经提供了关于主机状态的全面统计信息。现在,ping 是具体的:每个主机单独安排,并且与每个主机的 last_answer_time 紧密关联。如果某台主机响应缓慢,只有它的 ping 任务会等待,其他主机的 ping 任务会在其时间正常执行。如果某台主机负载较高,它的 last_answer_time 会单调递增,因此如果自上次查询时间以来已经发生了查询,并且 ping_interval 已过,就不会再执行实际的 ping。
另一个新特性是任务可以并行执行。例如,索引刷新可以同时对任意数量的索引执行,而不是串行执行。目前,这个数量设置为 2 个任务,但只是调优的问题。此外,当多个相似任务被安排时,现在可以限制它们的数量。例如,“malloc_trim”没有必要安排多次,因此它是一种“单例”——如果已经安排了一个,另一个尝试安排的请求会被丢弃。
通过这种任务管理方式,新增的特性源于现在所有任务都在一个队列中安排(而不是不同的线程组),并且我们确切知道它们何时运行。因此,现在可以显示此类统计信息,通过添加 'debug sched'、'debug tasks' 和 'debug systhreads' 实现。
第一个显示定时器的二叉堆:顶部的值表示下一个超时及其关联的任务;其他值依次显示(不过目前它们以原始二叉堆形式显示;如果需要,可以排序)。
'debug tasks' 显示守护进程中注册的所有任务及其统计信息和属性(可以并行运行多少此类任务;可以安排多少;当前正在执行多少;任务耗时;上次完成时间;执行次数;因队列过载被丢弃的次数;以及当前已排队的数量)。
最后,'debug systhreads' 显示工作线程的状态,如内部 ID、线程 ID、上次运行时间、总 CPU 时间、总 tick 数和完成的任务数、上一个任务耗时以及工作线程的空闲时间。如前所述,空闲 10 分钟会导致工作线程停止。
升级到 Manticore 3.1.0 或更新版本 以享受此更改