最近我们重新设计了系统线程。由于我们从Sphinx分叉出来,早期的searchd所有系统任务都采用经常唤醒的风格。每个服务都在专用线程中运行,每50毫秒唤醒一次并检查是否有任务需要执行。这意味着即使空闲的守护进程也会每秒唤醒200次“只是为了检查”任务。拥有6个此类任务会使每秒唤醒1200次,这在亚马逊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或更新版本 以享受此更改