在進程任務中升級
Appups 和 Relups 的小問題
在 Erlang 中,進行程式碼熱加載是最簡單的事情之一。您重新編譯,進行完全限定的函式呼叫,然後享受成果。然而,正確且安全地執行它要困難得多。
有一個非常簡單的挑戰使得程式碼重新載入變得有問題。讓我們運用我們驚人的 Erlang 編程腦袋,想像一個 gen_server 進程。此進程有一個 `handle_cast/2` 函式,它接受一種參數。我將其更新為接受不同種類參數的函式,編譯它,然後將其推送至生產環境。一切都很好,但由於我們有一個不想關閉的應用程式,我們決定將其載入生產 VM 以使其運行。

然後開始湧入一堆錯誤報告。結果證明,您不同的 `handle_cast` 函式不相容。因此,當它們被第二次呼叫時,沒有任何子句匹配。客戶很生氣,您的老闆也很生氣。然後,營運人員也很生氣,因為他必須到現場回滾程式碼、撲滅火災等等。如果您幸運的話,您就是那個營運人員。您熬夜並破壞了清潔工的夜晚(他通常喜歡哼著音樂跳舞,但在您面前他感到羞愧)。您很晚才回家,您的家人/朋友/魔獸世界團隊/孩子們對您很生氣,他們大吼大叫,摔門,您被獨自留下來。您曾保證不會出錯,不會有停機時間。畢竟您使用的是 Erlang,對吧?哦,但事實並非如此。您被獨自留下,蜷縮在廚房的角落裡,吃著冷凍的口袋餅。
當然,事情並不總是那麼糟糕,但重點仍然存在。如果您正在更改您的模組提供給外界的介面:更改內部資料結構、更改函式名稱、修改記錄(記住,它們是元組!),在生產系統上進行即時程式碼升級可能非常危險。它們都有可能導致崩潰。
當我們第一次嘗試程式碼重新載入時,我們有一個進程,其中包含一些隱藏的訊息,以處理進行完全限定呼叫。如果您還記得,一個進程可能看起來像這樣
loop(N) -> receive some_standard_message -> N+1; other_message -> N-1; {get_count, Pid} -> Pid ! N, loop(N); update -> ?MODULE:loop(N); end.
但是,如果我們更改 `loop/1` 的參數,這種做法不會解決我們的問題。我們需要將其擴展一下,如下所示
loop(N) -> receive some_standard_message -> N+1; other_message -> N-1; {get_count, Pid} -> Pid ! N, loop(N); update -> ?MODULE:code_change(N); end.
然後 `code_change/1` 可以負責呼叫 `loop` 的新版本。但是這種技巧無法用於通用迴圈。請看這個範例
loop(Mod, State) -> receive {call, From, Msg} -> {reply, Reply, NewState} = Mod:handle_call(Msg, State), From ! Reply, loop(Mod, NewState); update -> {ok, NewState} = Mod:code_change(State), loop(Mod, NewState) end.
看到問題了嗎?如果我們想更新 Mod 並載入新版本,就沒有辦法透過該實作安全地進行。呼叫 `Mod:handle_call(Msg, State)` 已經是完全限定的,而且很可能在我們重新載入程式碼和處理 `update` 訊息之間收到形式為 `{call, From, Msg}` 的訊息。在這種情況下,我們將以不受控制的方式更新模組。然後我們就會崩潰。
正確處理它的祕訣埋藏在 OTP 的內部。我們必須凍結時間的流逝!為此,我們需要更多的秘密訊息:將進程置於暫停狀態的訊息、更改程式碼的訊息,然後恢復您之前動作的訊息。在 OTP 行為的深處隱藏著一個特殊的協定,用於處理所有這些管理。這是透過稱為 `sys` 模組和第二個稱為 `release_handler` 的模組完成的,它們是 SASL(系統架構支援函式庫)應用程式的一部分。他們負責一切。
訣竅在於,您可以透過呼叫 `sys:suspend(PidOrName)` 來暫停 OTP 進程(您可以使用監督樹並查看每個監督者擁有的子進程來找到所有進程)。然後您使用 `sys:change_code(PidOrName, Mod, OldVsn, Extra)` 強制進程更新自身,最後,您呼叫 `sys:resume(PidOrName)` 使事情再次運作。
我們一直手動呼叫這些函式來編寫臨時腳本是不太實際的。相反,我們可以看看 relup 是如何完成的。
Erl 的第九圈

取得正在執行的版本,製作第二個版本並在執行時更新它,這個行為是危險的。表面上看起來只是簡單的組裝 _appups_(包含如何更新各個應用程式的指令的檔案)和 _relups_(包含更新整個版本的指令的檔案),但很快就會變成在 API 和未記載的假設之間掙扎。
我們正在進入 OTP 最複雜的部分之一,它難以理解和正確處理,而且非常耗時。事實上,如果您可以避免整個過程(從現在開始稱為 _relup_),並透過重新啟動 VM 和啟動新應用程式來執行簡單的滾動升級,我會建議您這樣做。Relups 應該是這些「不成功,便成仁」的工具之一。當您沒有其他選擇時才會使用。
處理版本升級時,有多個不同的層次
- 編寫 OTP 應用程式
- 將它們組合成一個版本
- 建立一個或多個 OTP 應用程式的新版本
- 建立一個 `appup` 檔案,說明如何變更以使舊應用程式和新應用程式之間的轉換有效
- 使用新應用程式建立新版本
- 從這些版本產生 relup 檔案
- 將新應用程式安裝在正在執行的 Erlang shell 中
每個步驟都可能比前一個步驟更複雜。我們在這裡只看到如何執行前 3 個步驟。為了能夠使用比以前的版本(嗯,誰在乎不用重新啟動就運行 regexes)更適合長期升級的應用程式,我們將介紹一個精彩的電子遊戲。
進度任務
進度任務 是一款革命性的角色扮演遊戲。我實際上稱之為 RPG 中的 OTP。如果您以前玩過 RPG,您會注意到許多步驟都很相似:四處奔走、殺死敵人、獲得經驗、獲得金錢、升級、獲得技能、完成任務。永遠重複。高手玩家會有捷徑,例如巨集,甚至是機器人,來四處奔走並為他們做事。
進度任務將所有這些通用步驟轉化為一個簡化的遊戲,您只需坐下來享受您的角色完成所有工作

在徵得這個奇妙遊戲的創作者 Eric Fredricksen 的同意後,我製作了一個非常簡約的 Erlang 克隆版本,稱為 _進程任務_。進程任務在原理上與進度任務相似,但它不是單人應用程式,而是一個能夠容納多個原始套接字連線(可透過 telnet 使用)的伺服器,讓人們可以使用終端機並暫時玩遊戲。
這個遊戲由以下部分組成
regis-1.0.0
regis 應用程式是一個進程註冊表。它具有與常規 Erlang 進程註冊表有些相似的介面,但它可以接受任何術語,並且旨在動態化。當所有呼叫進入伺服器時,它可能會使事情變慢,但它會比使用常規進程註冊表更好,因為常規進程註冊表不是為這種動態工作而設計的。如果本指南可以自動更新外部函式庫(工作量太大了),我會改用 gproc。它有幾個模組,即 regis.erl、regis_server.erl 和 regis_sup.erl。第一個是另外兩個(和應用程式回呼模組)的包裝器,`regis_server` 是主要的註冊 gen_server,而 `regis_sup` 是應用程式的監督者。
processquest-1.0.0
這是應用程式的核心。它包括所有遊戲邏輯。敵人、市場、殺戮場和統計資料。玩家本身是一個 gen_fsm,它會向自己發送訊息,以便不斷繼續前進。它包含的模組比 `regis` 多
- pq_enemy.erl
- 此模組隨機挑選一個敵人來戰鬥,形式為 `{<<“名稱”>>,[{drop,{<<“掉落名稱”>>,值}},{experience,經驗點}]}`。這讓玩家可以與敵人戰鬥。
- pq_market.erl
- 這實作了一個市場,允許找到具有給定值和給定強度的物品。返回的所有物品的形式為 `{<<“名稱”>>,修飾符,強度,值}`。有函式可以獲取武器、盔甲、盾牌和頭盔。
- pq_stats.erl
- 這是您角色的一個小屬性產生器。
- pq_events.erl
- gen_event 事件管理員的包裝器。這是一個通用樞紐,訂閱者透過自己的處理常式連線到該樞紐,以接收來自每個玩家的事件。它還負責等待給定的延遲時間,以避免遊戲瞬間完成。
- pq_player.erl
- 核心模組。這是一個 gen_fsm,它會經歷殺戮狀態迴圈,然後去市場,然後再次殺戮等等。它使用以上所有模組來運作。
- pq_sup.erl
- 位於一對 `pq_event` 和 `pq_player` 進程之上的監督者。它們都需要在一起才能運作,否則玩家進程將無用且被隔離,或者事件管理員永遠不會收到任何事件。
- pq_supersup.erl
- 應用程式的頂級監督者。它位於一堆 `pq_sup` 進程之上。這讓您可以產生任意數量的玩家。
- processquest.erl
- 包裝器和應用程式回呼模組。它為玩家提供基本介面:您啟動一個,然後訂閱事件。
sockserv-1.0.0

這是一個客製化的原始套接字伺服器,專為 processquest 應用程式設計。它會生成多個 gen_server,每個負責一個 TCP 套接字,將字串推送給某些客戶端。同樣地,您可以使用 telnet 來與其互動。雖然 telnet 技術上不是為原始套接字連線設計的,它本身是一個協議,但大多數現代客戶端都能毫無問題地接受它。以下是它的模組:
- sockserv_trans.erl
- 這個模組將從玩家的事件管理器接收到的訊息轉換為可列印的字串。
- sockserv_pq_events.erl
- 這是一個簡單的事件處理器,它會將來自玩家的任何事件傳送到套接字 gen_server。
- sockserv_serv.erl
- 這是一個 gen_server,負責接受連線、與客戶端通信並將資訊轉發給它。
- sockserv_sup.erl
- 這個模組監管多個套接字伺服器。
- sockserv.erl
- 這個模組是整個應用程式的回呼模組。
發布版本
我已將所有內容設置在名為 processquest 的目錄中,結構如下:
apps/ - processquest-1.0.0 - ebin/ - src/ - ... - regis-1.0.0 - ... - sockserv-1.0.0 - ... rel/ (will hold releases) processquest-1.0.0.config
基於此,我們可以建置一個發布版本。
注意: 如果您查看 processquest-1.0.0.config,您會看到其中包含了 crypto 和 sasl 等應用程式。Crypto 是為了確保偽隨機數產生器能正常初始化,而 SASL 則是在系統上執行應用程式升級 (appup) 所必須的。如果您忘記在發布版本中包含 SASL,將無法升級系統。
組態檔中出現了一個新的篩選器:{excl_archive_filters, [".*"]}
。這個篩選器確保不會生成 .ez
檔案,只會生成常規檔案和目錄。這是必要的,因為我們將使用的工具無法查看 .ez
檔案來找到所需的項目。
您也會看到沒有任何指示要求移除 debug_info
。如果沒有 debug_info
,執行應用程式升級將因某些原因而失敗。
按照上一章的說明,我們首先對所有應用程式呼叫 erl -make
。完成後,從 processquest
目錄啟動 Erlang shell,然後輸入以下內容:
1> {ok, Conf} = file:consult("processquest-1.0.0.config"), {ok, Spec} = reltool:get_target_spec(Conf), reltool:eval_target_spec(Spec, code:root_dir(), "rel"). ok
我們應該會有一個可用的發布版本。讓我們試試看。執行 ./rel/bin/erl -sockserv port 8888
(或您想要的任何其他埠號,預設為 8082)來啟動任何版本的虛擬機器。這會顯示很多關於進程啟動的日誌(這是 SASL 的功能之一),然後會出現一個常規的 Erlang shell。使用您想要的任何客戶端,在本機主機上啟動一個 telnet 連線:
$ telnet localhost 8888 Trying 127.0.0.1... Connected to localhost. Escape character is '^]'. What's your character's name? hakvroot Stats for your character: Charisma: 7 Constitution: 12 Dexterity: 9 Intelligence: 8 Strength: 5 Wisdom: 16 Do you agree to these? y/n
這對我來說有點太有智慧和魅力了。我輸入 n
然後按下 <Enter>
。
n Stats for your character: Charisma: 6 Constitution: 12 Dexterity: 12 Intelligence: 4 Strength: 6 Wisdom: 10 Do you agree to these? y/n
哦,是的,這很醜陋、愚蠢又軟弱。正是我想要以我為原型的英雄的樣子。
y Executing a Wildcat... Obtained Pelt. Executing a Pig... Obtained Bacon. Executing a Wildcat... Obtained Pelt. Executing a Robot... Obtained Chunks of Metal. ... Executing a Ant... Obtained Ant Egg. Heading to the marketplace to sell loot... Selling Ant Egg Got 1 bucks. Selling Goblin hair Got 1 bucks. ... Negotiating purchase of better equipment... Bought a plastic knife Heading to the killing fields... Executing a Pig... Obtained Bacon. Executing a Ant...
好的,對我來說夠了。輸入 quit
然後按下 <Enter>
來關閉連線。
quit Connection closed by foreign host.
如果您願意,您可以保持連線開啟,看看自己升級、獲得屬性等。遊戲基本上可以正常運作,您可以嘗試使用多個客戶端。它應該能持續運作,不會有問題。
很棒,對吧?嗯...
讓 Process Quest 變得更好

目前版本的 Process Quest 應用程式存在一些問題。首先,我們在擊敗的敵人類型方面變化很少。其次,文字看起來有點奇怪(正在執行一個螞蟻... 是怎麼回事)。第三個問題是遊戲有點太簡單了;讓我們為任務加入一個模式!另外一個問題是,物品的價值在真實遊戲中與你的等級直接相關,而我們的版本並沒有這樣做。最後,除非您閱讀程式碼並嘗試自行關閉客戶端,否則您不會發現,客戶端關閉連線會使玩家進程在伺服器上保持存活。哎呀,記憶體洩漏!
我必須解決這個問題!首先,我複製了需要修正的兩個應用程式。現在,我在其他應用程式之上有了 processquest-1.1.0
和 sockserv-1.0.1
(我使用 MajorVersion.Enhancements.BugFixes
的版本方案)。然後我實作了所有需要的變更。我不會詳細介紹所有這些變更,因為細節太多了,不適合本章的主旨 — 我們在這裡是要升級應用程式,而不是了解它的所有細微之處。如果您確實想了解所有細微之處,我已確保以合理的方式註解所有程式碼,讓您可以找到理解它所需的資訊。首先是針對 processquest-1.1.0
的變更。總而言之,pq_enemy.erl
、pq_events.erl
、pq_player.erl
都做了修改,並且我新增了一個名為 pq_quest.erl
的檔案,它會根據玩家殺死的敵人數量來實作任務。在這些檔案中,只有 pq_player.erl
的變更不相容,需要暫停執行。我所做的變更就是修改記錄:
-record(state, {name, stats, exp=0, lvlexp=1000, lvl=1, equip=[], money=0, loot=[], bought=[], time=0}).
改為這個:
-record(state, {name, stats, exp=0, lvlexp=1000, lvl=1, equip=[], money=0, loot=[], bought=[], time=0, quest}).
其中 quest
欄位將保存 pq_quest:fetch/0
給出的值。由於這個變更,我需要修改版本 1.1.0 中的 code_change/4
函式。實際上,我需要修改它兩次:一次是在升級時(從 1.0.0 移動到 1.1.0),另一次是在降級時(從 1.1.0 移動到 1.0.0)。幸運的是,OTP 在每種情況下都會傳遞不同的引數給我們。當我們升級時,我們會得到模組的版本號。我們此時並不特別關心它,很可能會直接忽略它。當我們降級時,我們會得到 {down, Version}
。這讓我們可以輕鬆匹配每個操作:
code_change({down, _}, StateName, State, _Extra) -> ...; code_change(_OldVsn, StateName, State, _Extra) -> ....
但是等等!我們不能像往常一樣盲目地獲取狀態。我們需要升級它。問題是,我們不能做類似這樣的事情:
code_change(_OldVsn, StateName, S = #state{}, _Extra) -> ....
我們有兩個選項。第一個選項是宣告一個新的狀態記錄,它將具有新的形式。我們最終會得到類似這樣的結果:
-record(state, {...}). -record(new_state, {...}).
然後我們必須更改模組每個函式子句中的記錄。這很煩人,而且不值得冒這個風險。更簡單的方法是將記錄展開為其底層的元組形式(請回顧常見資料結構簡介):
code_change({down, _}, StateName, #state{name=N, stats=S, exp=E, lvlexp=LE, lvl=L, equip=Eq, money=M, loot=Lo, bought=B, time=T}, _Extra) -> Old = {state, N, S, E, LE, L, Eq, M, Lo, B, T}, {ok, StateName, Old}; code_change(_OldVsn, StateName, {state, Name, Stats, Exp, LvlExp, Lvl, Equip, Money, Loot, Bought, Time}, _Extra) -> State = #state{ name=Name, stats=Stats, exp=Exp, lvlexp=LvlExp, lvl=Lvl, equip=Equip, money=Money, loot=Loot, bought=Bought, time=Time, quest=pq_quest:fetch() }, {ok, StateName, State}.
這就是我們的 code_change/4
函式!它所做的只是在兩種元組形式之間轉換。對於新版本,我們還會注意新增一個新的任務 — 如果新增任務但我們現有的所有玩家都無法使用它們,那就太無聊了。您會注意到我們仍然忽略了 _Extra 變數。這個變數是從 appup 檔案(稍後會描述)傳遞的,您將會選擇它的值。目前,我們並不關心它,因為我們只能升級和降級到一個版本。在某些更複雜的情況下,您可能希望在其中傳遞特定於發布版本的資訊。
對於 sockserv-1.0.1
應用程式,只有 sockserv_serv.erl
需要變更。幸運的是,它們不需要重新啟動,只需要匹配新的訊息。
兩個應用程式的兩個版本都已修正。但這還不足以讓我們繼續前進。我們必須找到一種方法讓 OTP 知道哪些類型的變更需要不同的動作。
Appup 檔案
Appup 檔案是需要執行以升級給定應用程式的 Erlang 命令列表。它們包含元組和原子列表,說明要執行什麼操作以及在什麼情況下執行。它們的一般格式為:
{NewVersion, [{VersionUpgradingFrom, [Instructions]}] [{VersionDownGradingTo, [Instructions]}]}.
它們要求版本列表,因為可以升級和降級到許多不同的版本。在我們的案例中,對於 processquest-1.1.0
,這會是:
{"1.1.0", [{"1.0.0", [Instructions]}], [{"1.0.0", [Instructions]}]}.
指令包含高階和低階命令。不過,我們通常只需要關心高階命令。
- {add_module, Mod}
- 模組 Mod 是第一次載入。
- {load_module, Mod}
- 模組 Mod 已載入虛擬機器中,並且已修改。
- {delete_module, Mod}
- 模組 Mod 已從虛擬機器中移除。
- {update, Mod, {advanced, Extra}}
- 這將暫停所有執行 Mod 的進程,使用 Extra 作為最後一個引數呼叫模組的
code_change
函式,然後恢復所有執行 Mod 的進程。Extra 可用於將任意資料傳遞到code_change
函式,以防升級時需要。 - {update, Mod, supervisor}
- 呼叫這個可以讓您重新定義監管器的
init
函式,以影響其重新啟動策略 (one_for_one
、rest_for_one
等) 或變更子進程規格 (這不會影響現有的進程)。 - {apply, {M, F, A}}
- 將呼叫
apply(M,F,A)
。 - 模組依賴
- 您可以使用
{load_module, Mod, [ModDependencies]}
或{update, Mod, {advanced, Extra}, [ModDeps]}
來確保命令僅在事先處理了其他模組之後才會執行。如果 Mod 和其依賴項不屬於同一個應用程式,則這特別有用。遺憾的是,沒有辦法為delete_module
指令提供類似的依賴關係。 - 新增或移除應用程式
- 在產生 relup 時,我們不需要任何特殊指令來移除或新增應用程式。產生
relup
檔案(用於升級發布版本的檔案)的函式會為我們處理此問題。
使用這些指令,我們可以為我們的應用程式編寫以下兩個 appup 檔案。檔案必須命名為 NameOfYourApp.appup
並放在應用程式的 ebin/
目錄中。以下是 processquest-1.1.0 的 appup 檔案
:
{"1.1.0", [{"1.0.0", [{add_module, pq_quest}, {load_module, pq_enemy}, {load_module, pq_events}, {update, pq_player, {advanced, []}, [pq_quest, pq_events]}]}], [{"1.0.0", [{update, pq_player, {advanced, []}}, {delete_module, pq_quest}, {load_module, pq_enemy}, {load_module, pq_events}]}]}.
您可以看到,我們需要新增新模組、載入兩個不需要暫停的模組,然後以安全的方式更新 pq_player
。當我們降級程式碼時,我們執行完全相同的操作,但順序相反。有趣的是,在其中一種情況下,{load_module, Mod}
會載入新版本,而在另一種情況下,它會載入舊版本。這一切都取決於升級和降級之間的上下文。
由於 sockserv-1.0.1
只有一個模組需要變更,且不需要暫停,因此它的 appup 檔案
只有:
{"1.0.1", [{"1.0.0", [{load_module, sockserv_serv}]}], [{"1.0.0", [{load_module, sockserv_serv}]}]}.
喔!下一步是使用新的模組建立新的版本。這是檔案 processquest-1.1.0.config
{sys, [ {lib_dirs, ["/Users/ferd/code/learn-you-some-erlang/processquest/apps"]}, {erts, [{mod_cond, derived}, {app_file, strip}]}, {rel, "processquest", "1.1.0", [kernel, stdlib, sasl, crypto, regis, processquest, sockserv]}, {boot_rel, "processquest"}, {relocatable, true}, {profile, embedded}, {app_file, strip}, {incl_cond, exclude}, {excl_app_filters, ["_tests.beam"]}, {excl_archive_filters, [".*"]}, {app, stdlib, [{incl_cond, include}]}, {app, kernel, [{incl_cond, include}]}, {app, sasl, [{incl_cond, include}]}, {app, crypto, [{incl_cond, include}]}, {app, regis, [{vsn, "1.0.0"}, {incl_cond, include}]}, {app, sockserv, [{vsn, "1.0.1"}, {incl_cond, include}]}, {app, processquest, [{vsn, "1.1.0"}, {incl_cond, include}]} ]}.
它是舊版本的複製/貼上,只變更了幾個版本號。首先,使用 erl -make
編譯兩個新的應用程式。如果您先前已下載 zip 檔案,它們就已經在那裡了。然後我們可以產生一個新的發行版本。首先,編譯兩個新的應用程式,然後輸入以下內容
$ erl -env ERL_LIBS apps/ 1> {ok, Conf} = file:consult("processquest-1.1.0.config"), {ok, Spec} = reltool:get_target_spec(Conf), reltool:eval_target_spec(Spec, code:root_dir(), "rel"). ok
別喝太多 Kool-Aid (別太盲從)
為什麼我們不直接使用 systools
?因為 systools 本身也有問題。首先,它會產生 appup 檔案,這些檔案有時會有奇怪的版本號,而且無法完美運作。它還會假設一個幾乎沒有文檔記錄的目錄結構,但與 reltool 使用的結構有些接近。然而,最大的問題是,它會使用您的預設 Erlang 安裝作為根目錄,這可能會在解壓縮東西時產生各種權限問題等等。
無論使用哪個工具,都沒有簡單的方法,我們需要大量的手動工作才能完成。因此,我們建立一個命令鏈,以相當複雜的方式使用這兩個模組,因為這樣最終會減少一點工作量。
但是等等,還需要更多手動工作!
- 複製
rel/releases/1.1.0/processquest.rel
為rel/releases/1.1.0/processquest-1.1.0.rel
。 - 複製
rel/releases/1.1.0/processquest.boot
為rel/releases/1.1.0/processquest-1.1.0.boot
。 - 複製
rel/releases/1.1.0/processquest.boot
為rel/releases/1.1.0/start.boot
。 - 複製
rel/releases/1.0.0/processquest.rel
為rel/releases/1.0.0/processquest-1.0.0.rel
。 - 複製
rel/releases/1.0.0/processquest.boot
為rel/releases/1.0.0/processquest-1.0.0.boot
。 - 複製
rel/releases/1.0.0/processquest.boot
為rel/releases/1.0.0/start.boot
。
現在我們可以產生 relup
檔案。要做到這一點,請啟動 Erlang shell 並呼叫以下指令
$ erl -env ERL_LIBS apps/ -pa apps/processquest-1.0.0/ebin/ -pa apps/sockserv-1.0.0/ebin/ 1> systools:make_relup("./rel/releases/1.1.0/processquest-1.1.0", ["rel/releases/1.0.0/processquest-1.0.0"], ["rel/releases/1.0.0/processquest-1.0.0"]). ok
由於 ERL_LIBS 環境變數只會尋找最新版本的應用程式,我們還需要在其中加入 -pa <舊應用程式的路徑>
,以便 systools 的 relup 產生器能夠找到所有內容。完成後,將 relup 檔案移動到 rel/releases/1.1.0/
。當更新程式碼時,將會查詢該目錄,以便找到正確的東西。然而,我們將會遇到一個問題,就是 release handler 模組將會依賴於它假設存在的一堆檔案,但這些檔案不一定會在那裡。

升級發行版本
很好,我們有了 relup 檔案。但在能夠使用它之前,還有一些事情要做。下一步是為整個新版本的發行版本產生一個 tar 檔案
2> systools:make_tar("rel/releases/1.1.0/processquest-1.1.0"). ok
該檔案將位於 rel/releases/1.1.0/
中。我們現在需要手動將它移動到 rel/releases
,並在移動時重新命名,以加入版本號。更多硬編碼的垃圾!$ mv rel/releases/1.1.0/processquest-1.1.0.tar.gz rel/releases/
是我們擺脫這種情況的方法。
現在,這個步驟您希望在啟動真正的生產應用程式之前的任何時候執行。這個步驟需要在您啟動應用程式之前完成,因為它會讓您在 relup 之後能夠回滾到初始版本。如果您不這樣做,您將只能將生產應用程式降級到比第一個版本更新的版本,而不能降級到第一個版本!
開啟一個 shell 並執行此操作
1> release_handler:create_RELEASES("rel", "rel/releases", "rel/releases/1.0.0/processquest-1.0.0.rel", [{kernel,"2.14.4", "rel/lib"}, {stdlib,"1.17.4","rel/lib"}, {crypto,"2.0.3","rel/lib"},{regis,"1.0.0", "rel/lib"}, {processquest,"1.0.0","rel/lib"},{sockserv,"1.0.0", "rel/lib"}, {sasl,"2.1.9.4", "rel/lib"}]).
該函數的一般格式為 release_handler:create_RELEASES(RootDir, ReleasesDir, Relfile, [{AppName, Vsn, LibDir}])
。這會在 rel/releases
目錄(或任何其他 ReleasesDir)內建立一個名為 RELEASES
的檔案,當 relup 尋找要重新載入的檔案和模組時,該檔案將包含有關您的發行版本的基本資訊。
我們現在可以開始執行舊版本的程式碼。如果您啟動 rel/bin/erl
,它將預設啟動 1.1.0 版本。這是因為我們在啟動 VM 之前建立了新的發行版本。為了進行此示範,我們需要使用 ./rel/bin/erl -boot rel/releases/1.0.0/processquest
啟動該版本。您應該會看到一切都正在啟動。啟動一個 telnet 用戶端以連線到我們的 socket 伺服器,以便我們可以查看正在進行的即時升級。
當您覺得準備好進行升級時,請前往目前正在執行 ProcessQuest 的 Erlang shell,並呼叫以下函數
1> release_handler:unpack_release("processquest-1.1.0"). {ok,"1.1.0"} 2> release_handler:which_releases(). [{"processquest","1.1.0", ["kernel-2.14.4","stdlib-1.17.4","crypto-2.0.3", "regis-1.0.0","processquest-1.1.0","sockserv-1.0.1", "sasl-2.1.9.4"], unpacked}, {"processquest","1.0.0", ["kernel-2.14.4","stdlib-1.17.4","crypto-2.0.3", "regis-1.0.0","processquest-1.0.0","sockserv-1.0.0", "sasl-2.1.9.4"], permanent}]
此處的第二個提示會告訴您,該版本已準備好升級,但尚未安裝或永久化。要安裝它,請執行
3> release_handler:install_release("1.1.0"). {ok,"1.0.0",[]} 4> release_handler:which_releases(). [{"processquest","1.1.0", ["kernel-2.14.4","stdlib-1.17.4","crypto-2.0.3", "regis-1.0.0","processquest-1.1.0","sockserv-1.0.1", "sasl-2.1.9.4"], current}, {"processquest","1.0.0", ["kernel-2.14.4","stdlib-1.17.4","crypto-2.0.3", "regis-1.0.0","processquest-1.0.0","sockserv-1.0.0", "sasl-2.1.9.4"], permanent}]
所以現在,應該正在執行 1.1.0 版本,但它仍然不是永久性的。不過,您可以讓您的應用程式以這種方式執行。呼叫以下函數以使其永久化
5> release_handler:make_permanent("1.1.0"). ok.
啊,糟糕。我們的一些程序現在正在中止(錯誤輸出已從上面的範例中移除)。除非您查看我們的 telnet 用戶端,它似乎確實升級成功。問題在於,sockserv 中所有正在等待連線的 gen_server 都無法監聽訊息,因為接受 TCP 連線是一個阻塞的操作。因此,當載入新版本的程式碼時,伺服器無法升級,並被 VM 終止。看看我們如何確認這一點
6> supervisor:which_children(sockserv_sup). [{undefined,<0.51.0>,worker,[sockserv_serv]}] 7> [sockserv_sup:start_socket() || _ <- lists:seq(1,20)]. [{ok,<0.99.0>}, {ok,<0.100.0>}, ... {ok,<0.117.0>}, {ok,<0.118.0>}] 8> supervisor:which_children(sockserv_sup). [{undefined,<0.112.0>,worker,[sockserv_serv]}, {undefined,<0.113.0>,worker,[sockserv_serv]}, ... {undefined,<0.109.0>,worker,[sockserv_serv]}, {undefined,<0.110.0>,worker,[sockserv_serv]}, {undefined,<0.111.0>,worker,[sockserv_serv]}]
第一個指令顯示,所有正在等待連線的子程序都已中止。剩下的程序將是那些具有活動連線的程序。這顯示了保持程式碼回應的重要性。如果我們的程序能夠接收訊息並根據這些訊息採取行動,那麼一切都會順利。

在最後兩個指令中,我只是啟動更多工作程序來解決這個問題。雖然這可行,但它需要執行升級的人員進行手動操作。無論如何,這遠非最佳。解決這個問題的更好方法是變更應用程式的運作方式,以使用一個監控程序來監看 sockserv_sup
有多少個子程序。當子程序的數量低於給定的閾值時,監控程序就會啟動更多子程序。另一種策略是變更程式碼,以便以每次阻塞幾秒鐘的方式接受連線,並在可以接收訊息的暫停後繼續重試。這會讓 gen_server 有時間根據需要升級自己,前提是您會在安裝發行版本和使其永久化之間等待正確的延遲時間。實作這兩種解決方案中的一種或兩種都留給讀者作為練習,因為我有點懶。這些類型的崩潰是您希望在實際系統上進行這些更新之前測試程式碼的原因。
無論如何,我們現在已經解決了這個問題,而且我們可能想要檢查升級程序是否順利進行
9> release_handler:which_releases(). [{"processquest","1.1.0", ["kernel-2.14.4","stdlib-1.17.4","crypto-2.0.3", "regis-1.0.0","processquest-1.1.0","sockserv-1.0.1", "sasl-2.1.9.4"], permanent}, {"processquest","1.0.0", ["kernel-2.14.4","stdlib-1.17.4","crypto-2.0.3", "regis-1.0.0","processquest-1.0.0","sockserv-1.0.0", "sasl-2.1.9.4"], old}]
這值得擊拳慶祝。您可以透過執行 release_handler:install(OldVersion).
來嘗試降級安裝。這應該可以順利運作,雖然它可能會導致更多從未更新自己的程序被終止。
別喝太多 Kool-Aid (別太盲從)
如果由於某些原因,當嘗試使用本章中顯示的技術回滾到發行版本的第一個版本時,回滾總是失敗,您可能忘記建立 RELEASES 檔案。如果您在呼叫 release_handler:which_releases()
時看到 {YourRelease,Version,[],Status}
中的空清單,您就可以知道這一點。這是一個清單,其中包含要載入和重新載入的模組的位置,它會在啟動 VM 並讀取 RELEASES 檔案時,或在解壓縮新的發行版本時首次建立。
好的,以下是擁有可運作 relup 所必須採取的全部動作的清單
- 為您的第一個軟體迭代撰寫 OTP 應用程式
- 編譯它們
- 使用 Reltool 建立發行版本 (1.0.0)。它必須包含偵錯資訊,且沒有
.ez
封存檔。 - 請確保您在啟動生產應用程式之前的某個時間點建立 RELEASES 檔案。您可以使用
release_handler:create_RELEASES(RootDir, ReleasesDir, Relfile, [{AppName, Vsn, LibDir}])
來完成此操作。 - 執行發行版本!
- 在其中找到錯誤
- 修復新版本應用程式中的錯誤
- 為每個應用程式撰寫
appup
檔案 - 編譯新的應用程式
- 建立新的發行版本(在我們的案例中為 1.1.0)。它必須包含偵錯資訊,且沒有
.ez
封存檔 - 將
rel/releases/NewVsn/RelName.rel
複製為rel/releases/NewVsn/RelName-NewVsn.rel
- 將
rel/releases/NewVsn/RelName.boot
複製為rel/releases/NewVsn/RelName-NewVsn.boot
- 將
rel/releases/NewVsn/RelName.boot
複製為rel/releases/NewVsn/start.boot
- 將
rel/releases/OldVsn/RelName.rel
複製為rel/releases/OldVsn/RelName-OldVsn.rel
- 將
rel/releases/OldVsn/RelName.boot
複製為rel/releases/OldVsn/RelName-OldVsn.boot
- 將
rel/releases/OldVsn/RelName.boot
複製為rel/releases/OldVsn/start.boot
- 使用
systools:make_relup("rel/releases/Vsn/RelName-Vsn", ["rel/releases/OldVsn/RelName-OldVsn"], ["rel/releases/DownVsn/RelName-DownVsn"]).
產生 relup 檔案 - 將 relup 檔案移動到
rel/releases/Vsn
- 使用
systools:make_tar("rel/releases/Vsn/RelName-Vsn").
產生新發行版本的 tar 檔案 - 將 tar 檔案移動到
rel/releases/
- 開啟一些仍然執行發行版本第一個版本的 shell
- 呼叫
release_handler:unpack_release("NameOfRel-Vsn").
- 呼叫
release_handler:install_release(Vsn).
- 呼叫
release_handler:make_permanent(Vsn).
- 請確保一切順利。如果沒有,請透過安裝較舊的版本來回滾。
您可能想要撰寫一些指令碼來自動化此過程。

再次強調,relup 是 OTP 中非常混亂的一部分,也是很難掌握的一部分。您可能會發現自己找到了許多新的錯誤,這些錯誤都比之前的錯誤更難以理解。對於您將如何執行某些操作做出了一些假設,而且在建立發行版本時選擇不同的工具會改變應該完成的操作。您可能會想使用 sys
模組的功能來撰寫自己的更新程式碼!或者使用像是 rebar3 之類的工具,它們會自動化一些痛苦的步驟。無論如何,本章及其範例都是根據作者所知的最佳知識撰寫的,這個人有時喜歡用第三人稱來談論自己。
如果可以透過不需要 relup 的方式來升級應用程式,我建議您這樣做。據說,使用 relup 的 Ericsson 部門花在測試它們的時間,與他們花在測試自己應用程式的時間一樣多。它們是一種用於處理絕對不能關閉的產品的工具。當您需要它們時,您就會知道,主要是因為您已準備好忍受使用它們的麻煩(必須喜歡這種循環邏輯!)當需要時,relup 完全有用。
我們現在去學習一些 Erlang 更友善的功能,如何?