檢視原始碼 組態和版本

在最後一個指南中,我們將為分散式鍵值儲存設定路由表,然後最後封裝軟體以進行生產。

讓我們開始吧。

應用程式環境

到目前為止,我們已將路由表硬編碼到 KV.Router 模組中。然而,我們希望讓表格動態化。這不僅允許我們設定開發/測試/生產,還允許不同的節點使用路由表中的不同條目執行。OTP 有個功能可以做到這一點:應用程式環境。

每個應用程式都有個環境,用於根據金鑰儲存應用程式的特定組態。例如,我們可以將路由表儲存在 :kv 應用程式環境中,給它一個預設值,並允許其他應用程式根據需要變更表格。

開啟 apps/kv/mix.exs 並變更 application/0 函式,以傳回下列內容

def application do
  [
    extra_applications: [:logger],
    env: [routing_table: []],
    mod: {KV, []}
  ]
end

我們已在應用程式中新增一個 :env 金鑰。它傳回應用程式的預設環境,其中包含金鑰 :routing_table 和空清單值的條目。應用程式環境隨附空表格是有意義的,因為特定的路由表取決於測試/部署結構。

為了在我們的程式碼中使用應用程式環境,我們需要將 KV.Router.table/0 取代為以下定義

@doc """
The routing table.
"""
def table do
  Application.fetch_env!(:kv, :routing_table)
end

我們使用 Application.fetch_env!/2 來讀取 :kv 環境中 :routing_table 的條目。您可以在 Application 模組中找到更多資訊和其他用於處理應用程式環境的函式。

由於我們的路由表現在是空的,我們的分散式測試應該會失敗。重新啟動應用程式並重新執行測試以查看失敗

$ iex --sname bar -S mix
$ elixir --sname foo -S mix test --only distributed

我們需要一種方式來設定應用程式環境。這就是我們使用設定檔的時候。

設定

設定檔提供一種機制,讓我們設定任何應用程式的環境。Elixir 提供兩個設定進入點

  • config/config.exs — 這個檔案會在建置時讀取,在我們編譯我們的應用程式之前,甚至在我們載入我們的相依關係之前。這表示我們無法存取我們應用程式中的程式碼,也無法存取我們的相依關係。然而,這表示我們可以控制它們是如何編譯的

  • config/runtime.exs — 這個檔案會在我們的應用程式和相依關係編譯後讀取,因此它可以設定我們的應用程式在執行時如何運作。如果你想讀取系統環境變數 (透過 System.get_env/1) 或任何類型的外部設定,這是執行此操作的適當位置

例如,我們可以將 IEx 預設提示字元設定為另一個值。讓我們使用以下內容建立 config/runtime.exs 檔案

import Config
config :iex, default_prompt: ">>>"

使用 iex -S mix 啟動 IEx,你會看到 IEx 提示字元已變更。

這表示我們也可以直接在 config/runtime.exs 檔案中設定我們的 :routing_table。然而,我們應該使用哪個設定值?

目前我們有兩個標記為 @tag :distributed 的測試。在 KVServerTest 中的「伺服器互動」測試,以及在 KV.RouterTest 中的「跨節點路由要求」。由於需要路由表,而目前路由表是空的,因此兩個測試都會失敗。

為了簡化,我們將定義一個總是指向目前節點的路由表。這是我們將用於開發和大多數測試的表格。回到 config/runtime.exs,新增這行程式碼

config :kv, :routing_table, [{?a..?z, node()}]

有了這麼簡單的表格,我們現在可以從 test/kv_server_test.exs 中的測試中移除 @tag :distributed。如果你執行完整的套件,測試現在應該會通過。

然而,對於 KV.RouterTest 中的測試,我們實際上需要兩個節點在我們的路由表中。為此,我們將撰寫一個設定區塊,在該檔案中的所有測試之前執行。設定區塊將變更應用程式環境,並在我們完成後將其還原,如下所示

defmodule KV.RouterTest do
  use ExUnit.Case

  setup_all do
    current = Application.get_env(:kv, :routing_table)

    Application.put_env(:kv, :routing_table, [
      {?a..?m, :"foo@computer-name"},
      {?n..?z, :"bar@computer-name"}
    ])

    on_exit fn -> Application.put_env(:kv, :routing_table, current) end
  end

  @tag :distributed
  test "route requests across nodes" do

請注意,我們已從 use ExUnit.Case 中移除 async: true。由於應用程式環境是全域儲存,修改它的測試無法同時執行。在所有變更就緒後,所有測試都應通過,包括分散式測試。

版本

現在我們的應用程式已分散式執行,您可能想知道我們如何封裝應用程式以在生產環境中執行。畢竟,我們到目前為止的所有程式碼都依賴於您目前系統中安裝的 Erlang 和 Elixir 版本。為了達成此目標,Elixir 提供版本。

版本是一個獨立目錄,包含您的應用程式程式碼、所有相依性,以及整個 Erlang 虛擬機器 (VM) 和執行時期。組建版本後,只要目標執行在與組建版本機器相同的作業系統 (OS) 發行版和版本上,就可以將其封裝並部署到目標。

在一般專案中,我們可以透過單純執行 mix release 來組建版本。不過,我們有一個傘式專案,在這種情況下,Elixir 需要我們提供一些額外輸入。讓我們看看需要什麼

$ MIX_ENV=prod mix release
** (Mix) Umbrella projects require releases to be explicitly defined with a non-empty applications key that chooses which umbrella children should be part of the releases:

releases: [
  foo: [
    applications: [child_app_foo: :permanent]
  ],
  bar: [
    applications: [child_app_bar: :permanent]
  ]
]

Alternatively you can perform the release from the children applications

這是因為傘式專案在部署軟體時提供了許多選項。我們可以

  • 將傘式專案中的所有應用程式部署到將同時作為 TCP 伺服器和鍵值儲存的節點

  • 部署 :kv_server 應用程式,只要路由表僅指向其他節點,就只作為 TCP 伺服器執行

  • 僅部署 :kv 應用程式,當我們希望節點僅作為儲存執行時(無 TCP 存取權)

作為起點,讓我們定義一個包含 :kv_server:kv 應用程式的版本。我們還會為其新增一個版本。在傘式專案根目錄中開啟 mix.exs,並在 def project 內新增

releases: [
  foo: [
    version: "0.0.1",
    applications: [kv_server: :permanent, kv: :permanent]
  ]
]

這定義了一個名為 foo 的版本,包含 kv_serverkv 應用程式。它們的模式設定為 :permanent,這表示如果這些應用程式發生故障,整個節點就會終止。這是合理的,因為這些應用程式對我們的系統至關重要。

在我們組裝發布版本之前,我們也來定義一下我們的生產環境路由表。假設我們預期會有兩個節點,我們需要更新 config/runtime.exs 成為這樣

import Config

config :kv, :routing_table, [{?a..?z, node()}]

if config_env() == :prod do
  config :kv, :routing_table, [
    {?a..?m, :"foo@computer-name"},
    {?n..?z, :"bar@computer-name"}
  ]
end

我們硬編碼了表和節點名稱,這對我們的範例來說已經夠用了,但你很可能會在實際的生產環境中將它移到外部設定系統。我們也用 config_env() == :prod 檢查將它包起來,因此這個設定不套用於其他環境。

設定就緒後,我們再來試著組裝發布版本

$ MIX_ENV=prod mix release foo
* assembling foo-0.0.1 on MIX_ENV=prod
* skipping runtime configuration (config/runtime.exs not found)

Release created at _build/prod/rel/foo!

    # To start your system
    _build/prod/rel/foo/bin/foo start

Once the release is running:

    # To connect to it remotely
    _build/prod/rel/foo/bin/foo remote

    # To stop it gracefully (you may also send SIGINT/SIGTERM)
    _build/prod/rel/foo/bin/foo stop

To list all commands:

    _build/prod/rel/foo/bin/foo

太棒了!一個發布版本組裝在 _build/prod/rel/foo。在發布版本內,將會有 bin/foo 檔案,它是你的系統的進入點。它支援多個指令,例如

  • bin/foo startbin/foo start_iexbin/foo restartbin/foo stop — 用於發布版本的通用管理

  • bin/foo rpc COMMANDbin/foo remote — 用於在執行中的系統上執行指令或連線到執行中的系統

  • bin/foo eval COMMAND — 用於啟動一個執行單一指令後就關閉的新系統

  • bin/foo daemonbin/foo daemon_iex — 用於在類 Unix 系統上將系統啟動為守護程式

  • bin/foo install — 用於在 Windows 電腦上將系統安裝為服務

如果你執行 bin/foo start,它會使用等於發布版本名稱的簡短名稱 (--sname) 來啟動系統,在這個案例中是 foo。下一步是啟動一個名為 bar 的系統,因此我們可以將 foobar 連接在一起,就像我們在上一章所做的那樣。但在我們達成這個目標之前,我們來談談發布版本的優點。

為什麼要發布版本?

發布版本允許開發人員預先編譯並將他們所有的程式碼和執行時期打包成一個單元。發布版本的優點是

  • 程式碼預載。VM 有兩個載入程式碼的機制:互動式和嵌入式。預設情況下,它以互動模式執行,在第一次使用模組時動態載入模組。你的應用程式第一次呼叫 Enum.map/2 時,VM 會找到 Enum 模組並載入它。有一個缺點。當你在生產環境中啟動一個新的伺服器時,它可能需要載入許多其他模組,導致第一個請求的回應時間異常激增。發布版本以嵌入模式執行,它會預先載入所有可用的模組,保證你的系統在啟動後就能處理請求。

  • 設定和自訂。發布版本讓開發人員可以精細地控制系統設定和用於啟動系統的 VM 旗標。

  • 獨立的。發行版本不需要將原始碼包含在您的生產成品中。所有程式碼都已預先編譯並封裝。發行版本甚至不需要您的伺服器上安裝 Erlang 或 Elixir,因為它們預設包含 Erlang VM 及其執行時期。此外,Erlang 和 Elixir 標準函式庫都已精簡,只保留您實際使用的部分。

  • 多個發行版本。您可以組裝具有不同組態的發行版本,針對每個應用程式,甚至針對完全不同的應用程式。

我們已撰寫有關發行版本的詳細文件,因此 請查看官方文件以取得更多資訊。現在,我們將繼續探討上述的一些功能。

組裝多個發行版本

到目前為止,我們已組裝名為 foo 的發行版本,但我們的路由表包含 foobar 的資訊。讓我們啟動 foo

$ _build/prod/rel/foo/bin/foo start
16:58:58.508 [info]  Accepting connections on port 4040

讓我們連線到它,並在另一個終端機中發出請求

$ telnet 127.0.0.1 4040
Trying 127.0.0.1...
Connected to localhost.
Escape character is '^]'.
CREATE bitsandpieces
OK
PUT bitsandpieces sword 1
OK
GET bitsandpieces sword
1
OK
GET shopping foo
Connection closed by foreign host.

當我們對名為「bitsandpieces」的儲存區進行操作時,我們的應用程式已經可以運作。但由於「shopping」儲存區會儲存在 bar 上,因此請求會失敗,因為 bar 不可用。如果您回到執行 foo 的終端機,您將會看到

17:16:19.555 [error] Task #PID<0.622.0> started from #PID<0.620.0> terminating
** (stop) exited in: GenServer.call({KV.RouterTasks, :"bar@computer-name"}, {:start_task, [{:"foo@josemac-2", #PID<0.622.0>, #PID<0.622.0>}, [#PID<0.622.0>, #PID<0.620.0>, #PID<0.618.0>], :monitor, {KV.Router, :route, ["shopping", KV.Registry, :lookup, [KV.Registry, "shopping"]]}], :temporary, nil}, :infinity)
    ** (EXIT) no connection to bar@computer-name
    (elixir) lib/gen_server.ex:1010: GenServer.call/3
    (elixir) lib/task/supervisor.ex:454: Task.Supervisor.async/6
    (kv) lib/kv/router.ex:21: KV.Router.route/4
    (kv_server) lib/kv_server/command.ex:74: KVServer.Command.lookup/2
    (kv_server) lib/kv_server.ex:29: KVServer.serve/1
    (elixir) lib/task/supervised.ex:90: Task.Supervised.invoke_mfa/2
    (stdlib) proc_lib.erl:249: :proc_lib.init_p_do_apply/3
Function: #Function<0.128611034/0 in KVServer.loop_acceptor/1>
    Args: []

現在讓我們為 :bar 定義一個發行版本。第一步可以在 mix.exs 中定義一個與 foo 完全相同的發行版本。此外,我們將設定兩個發行版本上的 cookie 選項為 weknoweachother,以便它們允許彼此連線。請參閱 分散式 Erlang 文件 以取得有關此主題的更多資訊

releases: [
  foo: [
    version: "0.0.1",
    applications: [kv_server: :permanent, kv: :permanent],
    cookie: "weknoweachother"
  ],
  bar: [
    version: "0.0.1",
    applications: [kv_server: :permanent, kv: :permanent],
    cookie: "weknoweachother"
  ]
]

現在讓我們組裝兩個發行版本

$ MIX_ENV=prod mix release foo
$ MIX_ENV=prod mix release bar

如果 foo 仍在執行,請停止它並重新啟動它以載入 cookie

$ _build/prod/rel/foo/bin/foo start

並在另一個終端機中啟動 bar

$ _build/prod/rel/bar/bin/bar start

您應該會看到類似於以下錯誤的錯誤發生 5 次,然後應用程式才會最終關閉

    17:21:57.567 [error] Task #PID<0.620.0> started from KVServer.Supervisor terminating
    ** (MatchError) no match of right hand side value: {:error, :eaddrinuse}
        (kv_server) lib/kv_server.ex:12: KVServer.accept/1
        (elixir) lib/task/supervised.ex:90: Task.Supervised.invoke_mfa/2
        (stdlib) proc_lib.erl:249: :proc_lib.init_p_do_apply/3
    Function: #Function<0.98032413/0 in KVServer.Application.start/2>
        Args: []

發生這種情況是因為發行版 foo 已在埠 4040 上監聽,而 bar 嘗試執行相同的動作!其中一個選項是將 :port 組態移至應用程式環境,就像我們對路由表所做的那樣,並為每個節點設定不同的埠。

但讓我們嘗試其他方法。我們讓 bar 發行版僅包含 :kv 應用程式。因此,它作為儲存空間運作,但不會有前端。將 :bar 資訊變更為此

releases: [
  foo: [
    version: "0.0.1",
    applications: [kv_server: :permanent, kv: :permanent],
    cookie: "weknoweachother"
  ],
  bar: [
    version: "0.0.1",
    applications: [kv: :permanent],
    cookie: "weknoweachother"
  ]
]

現在讓我們再次組建 bar

$ MIX_ENV=prod mix release bar

最後成功啟動它

$ _build/prod/rel/bar/bin/bar start

如果您再次連線到 localhost 並執行另一個要求,現在一切都應該運作,只要路由表包含正確的節點名稱。傑出!

透過發行版,我們能夠「切出專案的不同區塊」,並準備它們在製作環境中執行,全部封裝在單一目錄中。

組態發行版

發行版也提供內建掛勾,用於組態製作系統幾乎所有的需求

  • config/config.exs — 提供建置時間應用程式組態,在我們的應用程式編譯之前執行。此檔案通常會根據環境匯入組態檔案,例如 config/dev.exsconfig/prod.exs

  • config/runtime.exs — 提供執行時間應用程式組態。它會在每次發行版啟動時執行,並透過組態提供者進一步擴充。

  • rel/env.sh.eexrel/env.bat.eex — 範本檔案,會複製到每個發行版中,並在每個指令上執行,以設定環境變數,包括特定於 VM 的變數和一般環境。

  • rel/vm.args.eex — 範本檔案,會複製到每個發行版中,並提供 Erlang 虛擬機器和其他執行時間旗標的靜態組態。

正如我們所見,config/config.exsconfig/runtime.exs 會在發行版和一般 Mix 指令期間載入。另一方面,rel/env.sh.eexrel/vm.args.eex 則特定於發行版。我們來看看。

作業系統環境組態

每個發行版都包含一個環境檔案,在類 Unix 系統上命名為 env.sh,在 Windows 電腦上命名為 env.bat,在 Elixir 系統啟動之前執行。在此檔案中,您可以執行任何作業系統層級的程式碼,例如呼叫其他應用程式、設定環境變數等。其中一些環境變數甚至可以組態發行版本身的執行方式。

例如,發布使用簡短名稱(--sname)執行。但是,如果你想實際在生產環境中執行分散式鍵值儲存,你將需要多個節點,並使用 --name 選項啟動發布。我們可以在 env.shenv.bat 檔案中設定 RELEASE_DISTRIBUTION 環境變數來達成此目的。Mix 已經有一個範本可供我們自訂,所以讓我們請 Mix 將它們複製到我們的應用程式

$ mix release.init
* creating rel/vm.args.eex
* creating rel/remote.vm.args.eex
* creating rel/env.sh.eex
* creating rel/env.bat.eex

如果你開啟 rel/env.sh.eex,你將會看到

#!/bin/sh

# # Sets and enables heart (recommended only in daemon mode)
# case $RELEASE_COMMAND in
#   daemon*)
#     HEART_COMMAND="$RELEASE_ROOT/bin/$RELEASE_NAME $RELEASE_COMMAND"
#     export HEART_COMMAND
#     export ELIXIR_ERL_OPTIONS="-heart"
#     ;;
#   *)
#     ;;
# esac

# # Set the release to load code on demand (interactive) instead of preloading (embedded).
# export RELEASE_MODE=interactive

# # Set the release to work across nodes.
# # RELEASE_DISTRIBUTION must be "sname" (local), "name" (distributed) or "none".
# export RELEASE_DISTRIBUTION=name
# export RELEASE_NODE=<%= @release.name %>

跨節點工作的必要步驟已經作為範例註解掉。你可以取消註解最後兩行(移除開頭的 #)來啟用完整分發。

如果你使用 Windows,你必須開啟 rel/env.bat.eex,你會找到這個

@echo off
rem Set the release to load code on demand (interactive) instead of preloading (embedded).
rem set RELEASE_MODE=interactive

rem Set the release to work across nodes.
rem RELEASE_DISTRIBUTION must be "sname" (local), "name" (distributed) or "none".
rem set RELEASE_DISTRIBUTION=name
rem set RELEASE_NODE=<%= @release.name %>

再次取消註解最後兩行(移除開頭的 rem)來啟用完整分發。這樣就完成了!

VM 參數

rel/vm.args.eex 允許你指定控制 Erlang VM 及其執行時期運作方式的低階旗標。你可以指定條目,就像你在命令列中指定參數一樣,同時也支援程式碼註解。以下是預設產生的檔案

## Customize flags given to the VM: https://erlang.dev.org.tw/doc/man/erl.html
## -mode/-name/-sname/-setcookie are configured via env vars, do not set them here

## Increase number of concurrent ports/sockets
##+Q 65536

## Tweak GC to run more often
##-env ERL_FULLSWEEP_AFTER 10

你可以在 Erlang 文件中查看 VM 參數和旗標的完整清單

總結

在整個指南中,我們建立了一個非常簡單的分散式鍵值儲存,作為探索許多建構(例如通用伺服器、監督程式、任務、代理程式、應用程式等)的機會。不僅如此,我們還為整個應用程式撰寫了測試,熟悉了 ExUnit,並學習如何使用 Mix 建置工具來完成廣泛的任務。

如果您正在尋找可於生產環境中使用的分散式鍵值儲存,您絕對應該深入了解 Riak,它也在 Erlang VM 中執行。在 Riak 中,儲存區會進行複製,以避免資料遺失,而且他們使用 一致性雜湊 來將儲存區對應至節點,而非使用路由器。一致性雜湊演算法有助於減少在您的即時系統中加入新的儲存節點時需要遷移的資料量。

當然,Elixir 的用途遠遠不只分散式鍵值儲存。嵌入式系統、資料處理和資料擷取、網路應用程式、音訊/視訊串流系統等都是 Elixir 擅長的許多不同領域。我們希望本指南已讓您做好準備,探索這些領域或任何您可能希望將 Elixir 帶入的未來領域。

編碼愉快!