Erlang的调度器如何绑定到CPU

调度器绑定CPU

在启动Erlang虚拟机的时候可以通过参数来设置它的调度器以什么方式绑定CPU。这个参数是 +sbt,也可以用另一个参数 +stbt。它们的使用方法是:

+sbt 绑定类型

例如:

1
erl +sbt db

这两个参数的差异表现在对如下错误的处理方式上:

  • 某些不支持绑定CPU功能的平台上尝试绑定调度器到CPU
  • 没有可用的CPU拓扑。运行时系统无法自动侦测到CPU拓扑,并且我们也没有设置自定义的CPU拓扑。

当发生上述两种情况的时候,如果使用的是 +sbt 参数,则运行时系统会打印错误消息并拒绝启动;而如果使用的是 +stbt 参数,则运行时系统会忽略错误,并且用调度器不绑定CPU的方式启动。

当前有效的绑定类型如下:
u、ns、ts、ps、s、nnts、nnps、tnnps、db。

下面对上述有效类型进行说明。我的机器CPU信息如下:

有两个NUMA节点,每个节点有一个物理处理器,两个物理处理器为:p1和p2,每个物理处理器有4个核,每个核有两个线程。p1上的线程标识符为:第一个核{0,8},第二个核{1,9},第三个核{2,10},第四个核{3,11};p2上的线程标识符为:第一个核{4,12},第二个核{5,13},第三个核{6,14},第四个核{7,15}。

u:是 unbound 的首字母。调度器不绑定在某个CPU线程上,而是由操作系统决定调度器在那个CPU线程上执行以及什么时候迁移到别的cpu线程上。这是Erlang虚拟机的默认行为。

ns:代表 no_spread。标识符相近的调度器尽可能地绑定在相近的CPU线程上。调度器绑定到cpu线程的顺序是:{0,8,1,9,2,10,3,11,4,12,5,13,6,14,7,15}。

ts:代表 thread_spread。低标识符的调度器先绑定所有CPU核的第一个线程,然后再绑定所有CPU核的第二个线程,以此类推。调度器绑定到cpu线程的顺序是:{0,1,2,3,4,5,6,7,8,9,10,11,12,13,14,15}。

ps:代表 processor_spread。和ts方式一样,只是会跨物理处理器核间隔地绑定。调度器绑定到cpu线程的顺序是:{0,4,1,5,2,6,3,7,8,12,9,13,10,14,11,15}。

s:代表 spread。调度器尽可能地绑定到cpu线程上。与ns方式相似。调度器绑定到cpu线程的顺序是:{0,8,1,9,2,10,3,11,4,12,5,13,6,14,7,15}。

nnts:代表 no_node_thread_spread。与thread_spread方式相似。但是,如果有多个NUMA节点存在,则调度器会先把一个NUMA节点内的cpu线程全部绑定完,然后再去绑定下一个NUMA节点内的cpu线程。调度器绑定到cpu线程的顺序是:{0,1,2,3,8,9,10,11,4,5,6,7,12,13,14,15}。

nnps:代表 no_node_processor_spread。与processor_spread方式相似。但是,如果有多个NUMA节点存在,则调度器会先把一个NUMA节点内的cpu线程全部绑定完,然后再去绑定下一个NUMA节点内的cpu线程。调度器绑定到cpu线程的顺序是:{0,1,2,3,8,9,10,11,4,5,6,7,12,13,14,15}。

tnnps:代表 thread_no_node_processor_spread。是thread_spread和no_node_processor_spread方式的组合。调度器将在NUMA节点间顺序绑定cpu线程,但是会先在一个NUMA节点内的cpu核的同类线程都绑定完了再到下一个NUMA节点。调度器绑定到cpu线程的顺序是:{0,1,2,3,4,5,6,7,8,9,10,11,12,13,14,15}。

db:代表 default_bind。是调度器绑定cpu的默认方式。目前就是thread_no_node_processor_spread方式。将来可能会改变为别的方式。调度器绑定到cpu线程的顺序是:{0,1,2,3,4,5,6,7,8,9,10,11,12,13,14,15}。

自定义CPU拓扑

自定义CPU拓扑会覆盖任何自动监测到的CPU拓扑。将调度器绑定到逻辑处理器时会使用自定义的CPU拓扑。

自定义CPU拓扑的格式定义:

  • <Id> = integer(); when 0 =< <Id> =< 65535
  • <IdRange> = <Id>-<Id>
  • <IdOrIdRange> = <Id> | <IdRange>
  • <IdList> = <IdOrIdRange>,<IdOrIdRange> | <IdOrIdRange>
  • <LogicalIds> = L<IdList>
  • <ThreadIds> = T<IdList> | t<IdList>
  • <CoreIds> = C<IdList> | c<IdList>
  • <ProcessorIds> = P<IdList> | p<IdList>
  • <NodeIds> = N<IdList> | n<IdList>
  • <IdDefs> = <LogicalIds><ThreadIds><CoreIds><ProcessorIds><NodeIds> | <LogicalIds><ThreadIds><CoreIds><NodeIds><ProcessorIds>
    • <LogicalIds>必须是一个标识符列表。
    • 除<LogicalIds>之外至少还有一个其他的标识符类型也必须有一个标识符列表。
    • 所有标识符列表必须产生相同数量的标识符。
  • CpuTopology = <IdDefs>:<IdDefs> | <IdDefs>
  • <IdRange>可以递增也可以递减

大写字母表示真实标识符,小写字母表示仅用于描述拓扑的伪标识符。作为实际标识符传递的标识符可能被运行时系统用于访问特定硬件,如果它们不正确,行为是未定义的。由于在没有真实的逻辑CPU标识符的情况下定义CPU拓扑没有意义,所以不接受假的逻辑CPU标识符。线程,核心,处理器和节点标识符可以省略。如果省略,线程ID默认为t0,核心ID默认为c0,处理器ID默认为p0,节点ID将为未定义。每个逻辑处理器必须属于一个且只有一个NUMA节点,或者没有逻辑处理器必须属于任何NUMA节点。NUMA节点标识符是系统范围的。 也就是说,系统上的每个NUMA节点都必须具有唯一的标识符。处理器标识符也是系统范围的。 核心标识符是处理器范围的。 线程标识符是核心范围。标识符类型的顺序意味着CPU拓扑的层次结构。

标识符类型的顺序:

  • <LogicalId><ThreadIds><CoreIds><ProcessorIds><NodeIds>
  • <LogicalIds><ThreadIds><CoreIds><NodeIds><ProcessorIds>

只要每个逻辑处理器属于一个且只有一个NUMA节点,则CPU拓扑可由NUMA节点外部处理器和NUMA节点内部处理器一起组成。

Erlang查询CPU拓扑以及虚拟机调度器绑定CPU的函数

  • erlang:system_info(cpu_topology).
  • erlang:system_info(scheduler_bindings).

linux 的CPU信息查询和NUMA工具

  • lscpu
    • 显示CPU架构信息
  • mpstat -P ALL
    • 显示各个CPU负载情况
  • numactl –hardware
    • 显示各个NUMA节点的内存以及distance情况