八、集羣管理器(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
方式能精細且方便地定義集羣,值得推薦。