八、集群管理器(ClusterManager)
特意追加一节来介绍集群管理器。集群管理器也是一个抽象对象,类似于抽象数组AbstractArray对象。
Julia里的抽象对象相当于“模板”。这种模板可以添加自定义内部变量派生出许多子类型对象。抽象数组和集群管理器都是一种抽象对象。当我们在下文中提到ClusterManager时,要记得它是抽象对象,别搞糊涂了。
在Julia里,主进程和Worker联网组成的结构称为一个“集群”,是ClusterManager抽象对象的一个子类型,无论单机或多机。主进程和Worker的概念我们已经很熟悉了。主进程(master process)恒有PID=1,只有在这个进程上可以增加或移除其他进程。从这个角度上说,Julia的集群管理是单向的,集群管理器相当于主进程的助理。不过,任意两个进程之间都可以互相通讯,所以通讯是平等的。
集群可以在单台机器或多台机器上创建。为了避免混淆,我们称单台机器上的集群为“单机集群”,在多台机器上称为“多机集群”。单机的情况大概是相当于把单机虚拟为多机,故又称为“虚拟集群”。
Julia的集群管理器,是包括了管理单机集群和多机集群的一组函数和命令。前者就是我们已经认识的各种进程操作的汇总。后者也是一样的操作,但为了管理多机器而加入了一些特殊命令。
所谓的集群管理器的功能有三个方面:
- 在集群环境中启动Worker。
- 管理每个Worker的生命周期,比如发送中断信号。
- (可选)提供数据传输。
多机集群的进程间的连接基于内置的TCP/IP传输协议,连接过程的内部原理是这样的:
- 首先,在主进程上调用带有
ClusterManager
对象的addprocs
。 addprocs
调用某种合适的方法在合适的机器上启动所需数量的Worker。- 每个Worker会拨出一个空闲端口,把自己的host和端口信息写入
stdout
。 - 集群管理器读取每个Worker的
stdout
并传给主进程。 - 主进程解析信息并设置Worker的TCP/IP连接。
- 集群里的每个进程都会被告知其他进程的连接信息。
- 每个进程都与PID更小的所有进程连接,照此方式组成一个两两联通的网络。
这些过程都是隐式的,用户真正要做的显式操作就是addprocs(参数)
,分为三种情况:
- 假如参数为整数
n
,那么它会构建一个有n
个Worker的单机集群。为空则默认n=Sys.CPU_THREADS
,此时总进程数等于CPU逻辑线程数+1。 - 假如参数为
hostname::Array
,即各机器的hostname的数组,那么它会构建一个多机集群。在官方的标准库文档里,又写作addprocs(machines)
,其中machines
是一个由”机器参数“组成的向量,每个机器参数对应启动一台机器。机器参数的格式为:字符串machine_spec
或元组(machine_spec,count)
。具体地,字符串machine_spec=[user@]hostname[:port] [bind_addr[:port]]
,其中[]
表示可省略。hostname
是唯一必写的,user
默认为当前用户,port
默认为标准SSH端口,它们共同提供了主进程和目标Worker之间的连接信息。假如写了bind_addr[:port]
,那么其他Worker将可以通过bind_addr[:port]
连接到这个Worker上,也就是说这是一个自定义的额外地址和端口。元组参数的第二个元素count
是整数,表示要在该机器上创建几个Worker。如果令count=:auto
(注意冒号),那么会创建等于该机器CPU逻辑线程总数的Worker。 - 假如参数为
manager::ClusterManager
,那么会创建一个自定义的集群。这里的manager::ClusterManager
是一个自定义的ClusterManager。官方文档中给的例子是:扩展包ClusterManagers.jl
中通过一个自定义ClusterManager构造了一个所谓的“Beowulf集群”。具体怎么自定义恐怕要去翻翻源代码。
小贴士:可以在Windows或Linux的终端中输入
hostname
命令查看该机器的hostname。
最后,创建好集群后就可以按照前几节讲的命令操作各进程了,无论单机或多机。
还有个--machinefile
命令,用于在启动Julia REPL时连接各机器。用法为在终端输入:
julia --machinefile 文件路径
# 或简写为
julia -m 文件路径
其中文件路径
是指向一个自定义文件的路径。这个文件由自己创建,内容是每台机器的hostname或IP地址,一行只写一台机器。例如在julia可执行文件的目录里放一个名为machinefile
的文件然后:
julia --m ~/machinefile
其中machinefile
文件的内容是两台电脑的hostname:
host1
host2
现在我们有了两种方式创建多机集群:addprocs(machines)
和--machinefile
。我读到一篇博客在吐槽后一种方式,观点大概是酱紫:
--machinefile
在连接机器后自动在每台机器上创建等于CPU逻辑线程数的Worker。它允许额外添加一些自定义信息,但自由度不高,比如不能自定义网络拓扑和Julia可执行文件的位置。如果想完整地控制集群创建过程,应使用addprocs(machines)
。具体做法是:
- 首先创建一个
startupfile.jl
文件,把addprocs(machines)
写在文件里。 - 在终端中
julia -L startupfile.jl
。它会在Julia REPL启动时立即执行startupfile.jl
。
与每次手动 addprocs()
或把addprocs()
写在代码开头相比,这种方法在有多个程序要并行时显然方便得多。在startupfile.jl
里我们还可以精细地设定集群参数。以下是详细的演示:
假定有一个集群包含4个服务器。除了主服务器外,另外3台远程服务器分别为:
- host1,24核(实际上是逻辑线程,以下用“核”代指“逻辑线程”。)
- host2,12核
- host3,8核
machinefile
里写的是:
host1
host2
host3
假定需要的Worker数量等于核数,那么直接julia -m ~/machinefile
即可。如果你面对的是非常多服务器组成的超级计算机或超大集群,那么可设法生成一个machinefile
文件。具体生成办法请咨询MPI用户。
但是,如果你想搭建一个具有:master_slave
拓扑的集群,使得所有Worker只能与主进程通讯而不能互相通讯(在很多集群中经常要这么做),可以写一个startupfile.jl
文件,内容为:
- 先启动位于主服务器的Worker,比如
addprocs(4)
。注意这一步与下一步毫无关系,如果你不需要主服务器上有Worker,甚至可以省略这一步。(当然通常不希望主服务器闲着。) - 把3台远程服务器加入集群:
for host in ["host1","host2","host3"]
addprocs(host;topology=:master_slave) # 注意分号和冒号
end
# 或者写作
addprocs(["host1", "host2", "host3"]; topology=:master_slave)
这样每台服务器上会自动创建等于核数的Worker。也可以逐个规定Worker数量,只要把hostname
改成(hostname,数量)
:
addprocs([("host1", 24), ("host2", 12), ("host3", 8)]; topology=:master_slave)
如果服务器很多,写这么多hostname会很麻烦。可以先把hostname都写在一个machinefile
文件里,或用MPI用户的办法自动导出一个machinefile
文件(如果导出的文件里只有hostname没有Worker数量,只能自己再补上。不补上就是默认等于核数。),然后逐行读取:
addprocs(collect(eachline("~/machinefile")); topology=:master_slave)
除此之外,addprocs()
还有几个自定义参数,详见官方文档。
总之,用julia -L startupfile.jl
方式能精细且方便地定义集群,值得推荐。