檢視原始碼 版本處理

版本處理原則

Erlang 程式語言的一個重要特性是能夠在執行時更改模組程式碼,也就是程式碼替換,如 Erlang 參考手冊中的程式碼替換所述。

基於這個特性,OTP 應用程式 SASL 提供了一個框架,用於在執行時在整個版本的不同版本之間進行升級和降級。這稱為版本處理

該框架包含:

  • 離線支援 - systools 用於產生腳本和建立版本套件
  • 線上支援 - release_handler 用於解壓縮和安裝版本套件

基於 Erlang/OTP 的最小系統,能夠進行版本處理,因此包含 Kernel、STDLIB 和 SASL 應用程式。

版本處理工作流程

步驟 1) 按照版本中的說明建立版本。

步驟 2) 版本傳輸到目標環境並在那裡安裝。有關如何安裝第一個目標系統的資訊,請參閱系統原則

步驟 3) 在開發環境中對程式碼進行修改,例如錯誤修正。

步驟 4) 在某些時候,是時候建立一個新版本的版本了。相關的 .app 檔案會更新,並寫入新的 .rel 檔案。

步驟 5) 對於每個修改過的應用程式,都會建立一個應用程式升級檔案.appup。在此檔案中,描述如何在應用程式的舊版本和新版本之間進行升級和/或降級。

步驟 6) 根據 .appup 檔案,建立一個稱為 relup版本升級檔案。此檔案描述如何在整個版本的舊版本和新版本之間進行升級和/或降級。

步驟 7) 建立一個新的版本套件,並傳輸到目標系統。

步驟 8) 使用版本處理器解壓縮新的版本套件。

步驟 9) 安裝新版本的版本,也使用版本處理器。這是透過評估 relup 中的指示來完成的。可以新增、刪除或重新載入模組,可以啟動、停止或重新啟動應用程式等等。在某些情況下,甚至需要重新啟動執行時系統。

  • 如果安裝失敗,可以重新啟動系統。然後會自動使用舊的版本。
  • 如果安裝成功,則新版本會成為預設版本,如果重新啟動系統,則會使用此版本。

版本處理方面

Appup 食譜包含了 .appup 檔案的範例,這些檔案用於通常易於在執行時處理的升級/降級的典型案例。但是,許多方面會使版本處理變得複雜,例如:

  • 複雜或循環的相依性可能會使決定執行的順序變得困難,甚至不可能,而不會在升級或降級期間冒著執行時錯誤的風險。相依性可以是:

    • 節點之間
    • 程序之間
    • 模組之間
  • 在版本處理期間,不受影響的程序會繼續正常執行。這可能會導致逾時或其他問題。例如,在暫停使用特定模組的程序和載入此模組的新版本之間的時間視窗中建立的新程序,可以執行舊的程式碼。

因此,建議程式碼的變更應盡可能地小步進行,並且始終保持向後相容性。

需求

為了使版本處理正常運作,執行時系統必須知道它正在執行的版本。如果系統重新啟動,例如在失敗後由 heart 重新啟動,它還必須能夠(在執行時)變更要使用的啟動腳本和系統組態檔。因此,必須將 Erlang 作為嵌入式系統啟動;有關如何執行此操作的資訊,請參閱嵌入式系統。

為了使系統重新啟動正常運作,還需要使用心跳監控來啟動系統;請參閱 ERTS 中的 erl 和 Kernel 中的模組 heart

其他需求:

  • 版本套件中包含的啟動腳本必須從與版本套件本身相同的 .rel 檔案產生。

    當執行升級或降級時,會從腳本擷取有關應用程式的資訊。

  • 必須僅使用一個系統組態檔(稱為 sys.config)來組態系統。

    如果找到此檔案,則會在建立版本套件時自動包含它。

  • 除了第一個版本外,版本的所有版本都必須包含 relup 檔案。

    如果找到此檔案,則會在建立版本套件時自動包含它。

分散式系統

如果系統由多個 Erlang 節點組成,則每個節點都可以使用自己的版本。版本處理器是一個在本機註冊的程序,必須在需要升級或降級的每個節點上呼叫。可以使用版本處理指示 sync_nodes 來同步許多節點的版本處理器程序;請參閱 SASL 中的 appup

版本處理指示

OTP 支援一組在建立 .appup 檔案時使用的版本處理指示。版本處理器了解其中的一個子集,即低階指示。為了方便使用者,還有許多高階指示,這些指示由 systools:make_relup 轉換為低階指示。

本節說明了一些最常用的指示。指示的完整清單包含在 SASL 中的 appup 中。

首先,先定義一些術語:

  • 駐留模組 - 程序具有其尾遞迴迴圈函數的模組。如果這些函數在多個模組中實作,則所有這些模組都是該程序的駐留模組。
  • 功能模組 - 不是任何程序的駐留模組的模組。

對於使用 OTP 行為實作的程序,行為模組是該程序的駐留模組。回呼模組是一個功能模組。

load_module

如果對功能模組進行了簡單的擴充,則只需將新版本的模組載入系統,然後移除舊版本即可。這稱為簡單程式碼替換,為此使用以下指示:

{load_module, Module}

update

如果進行了更複雜的變更,例如變更 gen_server 的內部狀態格式,則簡單的程式碼替換是不夠的。相反,有必要:

  • 暫停使用該模組的程序(以避免它們在程式碼替換完成之前嘗試處理任何請求)。
  • 要求它們轉換內部狀態格式,並切換到新版本的模組。
  • 移除舊版本。
  • 恢復程序。

這稱為同步程式碼替換,為此使用以下指示:

{update, Module, {advanced, Extra}}
{update, Module, supervisor}

當變更如上所述的行為的內部狀態時,會使用帶有引數 {advanced,Extra}update。它會導致行為程序呼叫回呼函數 code_change/3,傳遞詞組 Extra 和其他一些資訊作為引數。請參閱各個行為的手冊頁和Appup 食譜

當變更監管程式的啟動規格時,會使用帶有引數 supervisorupdate。請參閱Appup 食譜

當要更新模組時,版本處理器會透過遍歷每個執行中應用程式的監管樹,並檢查所有子規格,來找到使用該模組的程序。

{Id, StartFunc, Restart, Shutdown, Type, Modules}

如果程序子規格中的 Modules 中列出了名稱,則該程序會使用該模組。

如果 Modules=dynamic(事件管理員的情況),則事件管理員程序會通知版本處理器有關目前安裝的事件處理器 (gen_event) 清單,並檢查模組名稱是否在此清單中。

版本處理器會透過分別呼叫函數 sys:suspend/1,2sys:change_code/4,5sys:resume/1,2 來暫停、要求程式碼變更和恢復程序。

add_module 和 delete_module

如果引入了新模組,則會使用以下指示:

{add_module, Module}

此指示會載入模組 Module。在嵌入式模式下執行 Erlang 時,必須使用此指示。在互動模式下執行 Erlang 時,並非嚴格需要此指示,因為程式碼伺服器會自動搜尋和載入未載入的模組。

add_module 的相反指示是 delete_module,它會卸載模組:

{delete_module, Module}

當評估指示時,任何應用程式中、以 Module 作為駐留模組的任何程序都會被終止。因此,使用者必須確保在刪除模組 Module 之前終止所有此類程序,以避免發生監管程式重新啟動失敗的情況。

應用程式指示

以下是新增應用程式的指示:

{add_application, Application}

新增應用程式意味著使用多個 add_module 指示載入 .app 檔案中 modules 鍵定義的模組,然後啟動應用程式。

以下是移除應用程式的指示:

{remove_application, Application}

移除應用程式意味著應用程式已停止,使用多個 delete_module 指示卸載模組,然後從應用程式控制器卸載應用程式規格。

以下是重新啟動應用程式的指示:

{restart_application, Application}

重新啟動應用程式意味著應用程式已停止,然後再次啟動,類似於依序使用指示 remove_applicationadd_application

apply (低階)

若要從版本處理器呼叫任意函數,則會使用以下指示:

{apply, {M, F, A}}

版本處理器會評估 apply(M, F, A)

restart_new_emulator (低階)

當變更為新版本的執行時系統,或升級任何核心應用程式 Kernel、STDLIB 或 SASL 時,會使用此指示。如果由於其他原因需要重新啟動系統,則應改為使用 restart_emulator 指示。

此指令要求系統啟動時需啟用心跳監控;請參閱 ERTS 中的 erl 和 Kernel 中的 heart 模組。

restart_new_emulator 指令必須永遠是 relup 中的第一個指令。如果 relup 是由 systools:make_relup/3,4 產生,則會自動符合此條件。

當 release handler 遇到此指令時,它會先產生一個臨時的啟動檔案,該檔案會啟動新版本的運行系統和核心應用程式,以及所有其他應用程式的舊版本。然後,它會呼叫 init:reboot/0 來關閉目前運行的系統實例。所有程序都會優雅地終止,然後系統會由 heart 程式使用臨時啟動檔案重新啟動。重新啟動後,會執行 relup 中的其餘指令。這是作為臨時啟動腳本的一部分完成的。

警告

此機制會導致新版本的運行系統和核心應用程式在啟動時與其他應用程式的舊版本一起執行。因此,請格外小心,避免不相容性。在某些情況下,核心應用程式中不相容的變更可能是必要的。如果可能,此類變更會在實際變更之前,經過兩個主要版本的棄用程序。為了確保應用程式不會因不相容的變更而崩潰,請盡快移除任何對已棄用函數的呼叫。

升級完成時,會寫入一個資訊報告。若要以程式設計方式判斷升級是否完成,請呼叫 release_handler:which_releases(current) 並檢查它是否傳回預期的 (即新的) 發行版本。

當新的運行系統開始運作時,必須將新的發行版本設定為永久版本。否則,如果系統再次重新啟動,將會使用舊版本。

在 UNIX 上,release handler 會告知 heart 程式要使用哪個命令重新啟動系統。環境變數 HEART_COMMAND 通常由 heart 程式使用,在此情況下會被忽略。相反地,命令預設為 $ROOT/bin/start。可以使用 SASL 設定參數 start_prg 設定其他命令。如需更多資訊,請參閱 SASL

restart_emulator (低階)

此指令與 ERTS 或任何核心應用程式的升級無關。任何應用程式都可以使用它來強制在執行完所有升級指令後重新啟動運行系統。

relup 腳本只能包含一個 restart_emulator 指令,而且必須永遠放在最後。如果 relup 是由 systools:make_relup/3,4 產生,則會自動符合此條件。

當 release handler 遇到此指令時,它會呼叫 init:reboot/0 來關閉運行系統。所有程序都會優雅地終止,然後系統可以由 heart 程式使用新的發行版本重新啟動。重新啟動後,不會執行更多升級指令。

應用程式升級檔案

若要定義如何在應用程式的目前版本和先前版本之間升級/降級,需要建立一個應用程式升級檔案,簡稱 .appup 檔案。該檔案應命名為 Application.appup,其中 Application 是應用程式的名稱。

{Vsn,
 [{UpFromVsn1, InstructionsU1},
  ...,
  {UpFromVsnK, InstructionsUK}],
 [{DownToVsn1, InstructionsD1},
  ...,
  {DownToVsnK, InstructionsDK}]}.
  • Vsn,一個字串,是應用程式的目前版本,如 .app 檔案中所定義。
  • 每個 UpFromVsn 都是要從中升級的應用程式先前版本。
  • 每個 DownToVsn 都是要降級到的應用程式先前版本。
  • 每個 Instructions 都是發行版本處理指令的清單。

UpFromVsnDownToVsn 也可以指定為正規表示式。如需有關 .appup 檔案語法和內容的更多資訊,請參閱 SASL 中的 appup

Appup 食譜 包含典型升級/降級案例的 .appup 檔案範例。

範例: 考慮來自 發行版本 的發行版本 ch_rel-1。假設您想在伺服器 ch3 中新增一個函數 available/0,該函數會傳回可用的頻道數量 (在嘗試此範例時,請在原始目錄的副本中進行變更,以確保第一個版本仍然可用)

-module(ch3).
-behaviour(gen_server).

-export([start_link/0]).
-export([alloc/0, free/1]).
-export([available/0]).
-export([init/1, handle_call/3, handle_cast/2]).

start_link() ->
    gen_server:start_link({local, ch3}, ch3, [], []).

alloc() ->
    gen_server:call(ch3, alloc).

free(Ch) ->
    gen_server:cast(ch3, {free, Ch}).

available() ->
    gen_server:call(ch3, available).

init(_Args) ->
    {ok, channels()}.

handle_call(alloc, _From, Chs) ->
    {Ch, Chs2} = alloc(Chs),
    {reply, Ch, Chs2};
handle_call(available, _From, Chs) ->
    N = available(Chs),
    {reply, N, Chs}.

handle_cast({free, Ch}, Chs) ->
    Chs2 = free(Ch, Chs),
    {noreply, Chs2}.

現在必須建立一個新版本的 ch_app.app 檔案,其中版本已更新

{application, ch_app,
 [{description, "Channel allocator"},
  {vsn, "2"},
  {modules, [ch_app, ch_sup, ch3]},
  {registered, [ch3]},
  {applications, [kernel, stdlib, sasl]},
  {mod, {ch_app,[]}}
 ]}.

若要將 ch_app"1" 升級到 "2" (以及從 "2" 降級到 "1"),您只需要載入新 (舊) 版本的 ch3 回呼模組。在 ebin 目錄中建立應用程式升級檔案 ch_app.appup

{"2",
 [{"1", [{load_module, ch3}]}],
 [{"1", [{load_module, ch3}]}]
}.

發行版本升級檔案

若要定義如何在發行版本的新版本和先前版本之間升級/降級,需要建立一個發行版本升級檔案,簡稱 .relup 檔案。

此檔案不需要手動建立。它可以由 systools:make_relup/3,4 產生。相關版本的 .rel 檔案、.app 檔案和 .appup 檔案會用作輸入。程式會推斷哪些應用程式要新增和刪除,以及哪些應用程式必須升級和/或降級。此操作的指令會從 .appup 檔案中擷取,並轉換為正確順序的單一低階指令清單。

如果 relup 檔案相對簡單,則可以手動建立。它只需要包含低階指令。

如需有關發行版本升級檔案語法和內容的詳細資訊,請參閱 SASL 中的 relup

範例,延續自上一節: 您有一個新版本的 ch_app "2" 和一個 .appup 檔案。也需要新版本的 .rel 檔案。這次檔案名為 ch_rel-2.rel,並且發行版本字串從 "A" 變更為 "B"

{release,
 {"ch_rel", "B"},
 {erts, "14.2.5"},
 [{kernel, "9.2.4"},
  {stdlib, "5.2.3"},
  {sasl, "4.2.1"},
  {ch_app, "2"}]
}.

現在可以產生 relup 檔案

1> systools:make_relup("ch_rel-2", ["ch_rel-1"], ["ch_rel-1"]).
ok

這會產生一個 relup 檔案,其中包含如何從版本 "A" ("ch_rel-1") 升級到版本 "B" ("ch_rel-2"),以及如何從版本 "B" 降級到版本 "A" 的指令。

新舊版本的 .app.rel 檔案都必須在程式碼路徑中,以及 .appup 和 (新的) .beam 檔案。可以使用選項 path 來擴展程式碼路徑

1> systools:make_relup("ch_rel-2", ["ch_rel-1"], ["ch_rel-1"],
[{path,["../ch_rel-1",
"../ch_rel-1/lib/ch_app-1/ebin"]}]).
ok

安裝發行版本

當您建立新版本的發行版本時,可以使用這個新版本建立發行版本套件,並將其傳輸到目標環境。

若要在執行階段安裝新版本的發行版本,請使用發行版本處理常式。這是屬於 SASL 應用程式的程序,負責處理發行版本套件的解壓縮、安裝和移除。release_handler 模組會與此程序進行通訊。

假設有一個可運作的目標系統,其安裝根目錄為 $ROOT,則應將新版本的發行版本套件複製到 $ROOT/releases

首先,解壓縮發行版本套件。然後從套件中提取檔案

release_handler:unpack_release(ReleaseName) => {ok, Vsn}
  • ReleaseName 是發行版本套件的名稱,但不包括 .tar.gz 副檔名。
  • Vsn 是已解壓縮發行版本的版本,如其 .rel 檔案中所定義。

會建立一個目錄 $ROOT/lib/releases/Vsn,其中放置 .rel 檔案、啟動腳本 start.boot、系統設定檔案 sys.configrelup。對於具有新版本號碼的應用程式,應用程式目錄會放置在 $ROOT/lib 下。未變更的應用程式不會受到影響。

可以安裝已解壓縮的發行版本。然後,發行版本處理常式會逐步評估 relup 中的指令

release_handler:install_release(Vsn) => {ok, FromVsn, []}

如果在安裝期間發生錯誤,則系統會使用舊版本的發行版本重新啟動。如果安裝成功,則系統之後會使用新版本的發行版本,但如果發生任何問題且系統重新啟動,它會再次開始使用先前的版本。

若要將新安裝的發行版本設為預設版本,必須將其設定為永久版本,這表示先前的版本會變成版本

release_handler:make_permanent(Vsn) => ok

系統會在 $ROOT/releases/RELEASES$ROOT/releases/start_erl.data 檔案中保留有關哪些版本是舊版和永久版本的資訊。

若要從 Vsn 降級到 FromVsn,必須再次呼叫 install_release

release_handler:install_release(FromVsn) => {ok, Vsn, []}

可以移除已安裝但未永久化的發行版本。然後,會從 $ROOT/releases/RELEASES 中刪除有關該發行版本的資訊,並移除特定於該發行版本的程式碼,即新的應用程式目錄和 $ROOT/releases/Vsn 目錄。

release_handler:remove_release(Vsn) => ok

範例 (延續自前幾節)

步驟 1) 建立一個目標系統,如 發行版本ch_rel 的第一個版本 "A" 的系統原理所述。這次,sys.config 必須包含在發行版本套件中。如果不需要任何設定,則該檔案應包含空清單

[].

步驟 2) 以簡單的目標系統啟動系統。實際上,它應作為嵌入式系統啟動。但是,使用具有正確啟動腳本和設定檔的 erl 對於說明目的來說已足夠

% cd $ROOT
% bin/erl -boot $ROOT/releases/A/start -config $ROOT/releases/A/sys
...

$ROOT 是目標系統的安裝目錄。

步驟 3) 在另一個 Erlang shell 中,產生啟動腳本並為新版本 "B" 建立發行版本套件。請記住包含 (可能更新的) sys.configrelup 檔案。如需更多資訊,請參閱 發行版本升級檔案

1> systools:make_script("ch_rel-2").
ok
2> systools:make_tar("ch_rel-2").
ok

現在,新的發行版本套件也包含 ch_app 的版本 "2" 和 relup 檔案

% tar tf ch_rel-2.tar
lib/kernel-9.2.4/ebin/kernel.app
lib/kernel-9.2.4/ebin/application.beam
...
lib/stdlib-5.2.3/ebin/stdlib.app
lib/stdlib-5.2.3/ebin/argparse.beam
...
lib/sasl-4.2.1/ebin/sasl.app
lib/sasl-4.2.1/ebin/sasl.beam
...
lib/ch_app-2/ebin/ch_app.app
lib/ch_app-2/ebin/ch_app.beam
lib/ch_app-2/ebin/ch_sup.beam
lib/ch_app-2/ebin/ch3.beam
releases/B/start.boot
releases/B/relup
releases/B/sys.config
releases/B/ch_rel-2.rel
releases/ch_rel-2.rel

步驟 4) 將發行版本套件 ch_rel-2.tar.gz 複製到 $ROOT/releases 目錄。

步驟 5) 在執行中的目標系統中,解壓縮發行版本套件

1> release_handler:unpack_release("ch_rel-2").
{ok,"B"}

新的應用程式版本 ch_app-2 會安裝在 $ROOT/lib 下,與 ch_app-1 相鄰。kernelstdlibsasl 目錄不會受到影響,因為它們沒有變更。

$ROOT/releases 下,會建立一個新目錄 B,其中包含 ch_rel-2.relstart.bootsys.configrelup

步驟 6) 檢查函數 ch3:available/0 是否可用

2> ch3:available().
** exception error: undefined function ch3:available/0

步驟 7) 安裝新版本。執行 $ROOT/releases/B/relup 中的指令,一個接一個,結果是載入新版本的 ch3。函式 ch3:available/0 現在可用。

3> release_handler:install_release("B").
{ok,"A",[]}
4> ch3:available().
3
5> code:which(ch3).
".../lib/ch_app-2/ebin/ch3.beam"
6> code:which(ch_sup).
".../lib/ch_app-1/ebin/ch_sup.beam"

ch_app 中,程式碼尚未更新的程序,例如監督者,仍然在評估 ch_app-1 的程式碼。

步驟 8) 如果現在重新啟動目標系統,它會再次使用 "A" 版本。必須將 "B" 版本設為永久,以便在系統重新啟動時使用。

7> release_handler:make_permanent("B").
ok

更新應用程式規格

當安裝新版本的發行版本時,會自動更新所有已載入應用程式的應用程式規格。

注意

關於新應用程式規格的資訊是從發行套件中包含的啟動腳本獲取的。因此,重要的是啟動腳本是從與用於建置發行套件的 .rel 檔案相同的檔案產生的。

具體來說,應用程式組態參數會根據(優先順序遞增)自動更新

  • 從新的應用程式資源檔案 App.app 中獲取的啟動腳本中的資料
  • 新的 sys.config
  • 命令列參數 -App Par Val

這表示在其他系統組態檔案中設定的參數值,以及使用 application:set_env/3 設定的值會被忽略。

當已安裝的發行版本設定為永久時,系統程序 init 會被設定為指向新的 sys.config

安裝後,應用程式控制器會比較所有執行中應用程式的新舊組態參數,並呼叫回呼函式

Module:config_change(Changed, New, Removed)
  • Module 是由 .app 檔案中的 mod 鍵定義的應用程式回呼模組。
  • ChangedNew 分別是所有已變更和新增組態參數的 {Par,Val} 列表。
  • Removed 是所有已移除參數 Par 的列表。

此函式是可選的,在實作應用程式回呼模組時可以省略。