版本

版本

2019 年 9 月 24 日

高層級的 OTP 中,我們從高處著眼於如何將應用程式結合到一個版本中,而在 開發 的後續章節中,我們建立並測試了 OTP 應用程式。我們已討論過 Erlang 系統與許多其他語言(其具有專案程式碼的單一進入點,例如程式啟動時會呼叫的 main() 函式)之間的不同設定方式。啟動執行程式碼的 Erlang 節點最終與其他語言和執行時期相當類似,而且可以打包成 Docker 容器,並像任何其他容器一樣執行,一旦透過啟動容器執行後,就不會顯得太不一樣。然而,取得一個已綑綁版本,其中包含易於使用的 shell 腳本或準備執行的 Docker 映象時,一開始可能會感到困惑,且會讓人覺得 Erlang 太過不同或是在您的環境中執行過於困難。

本章將提供一些低階細節,說明如何建構版本,但主要目的在於利用 service_discovery 專案,展示如何使用 rebar3 建立適合在地端開發的版本,然後建立一個準備製作的版本,最後展示如何使用 rebar3 提供的工具組態和操作版本。我們將展示基於並行執行應用程式而啟動的系統所提供的彈性優點,並不需要奇特的作業付出代價。

實質部分

回到作業系統比較,啟動一個 Erlang 版本類似於作業系統開機順序,其後接序由 init 系統啟動服務。Erlang 節點透過執行 開機檔 中的指令來啟動。這些指令會載入模組並啟動應用程式。Erlang 提供可從所有必要應用程式的清單中產生開機腳本的函式,其中定義於擴充功能為「.rel」的版本資源檔中,以及每個應用程式都擁有的相應應用程式資源檔「.app」檔。應用程式資源檔定義開機腳本必須載入的模組和每個應用程式的依賴關係,這樣開機腳本才能正確順序啟動應用程式。當只將版本中使用的應用程式與開機腳本綑綁在一起,然後複製並安裝到目標上時,稱為目標系統

在 Erlang/OTP 早期階段,只有 systools 和其函數可用於由發佈資源檔案產生開機指令碼。當時,發佈處理是手動程序,使用者會根據這個程序建構自己的工具組。然後產生了 reltool,一個隨 Erlang/OTP 附帶的發佈管理工具,用於簡化發佈產生,甚至還有一個 GUI。然而,建立和安裝目標系統從未在 sasl 應用程式的範例模組中提供,sasl/examples/src/target_system.erl

發佈持續對許多使用者來說很神秘且難以建構。relx 的產生目標是讓發佈產生和管理簡單到使用者不再覺得那是最好不要觸碰的負擔,部分作法是設定最少程度的組態就能開始,並在產生的發佈中包含執行階段管理工具。在 Rebar3 開始時,其結合了 relx 以提供其發佈建置功能。

除了建置和封裝之外,relx 還附有啟動發佈並與執行中發佈互動的 shell 指令碼。雖然執行發佈可以像 erl 一樣簡單(它本身執行從 start.script 建置的開機指令碼,可以在 Erlang 安裝的 bin/ 目錄中找到),提供的指令碼會處理設定適切的參數以指向設定檔、將遠端主控台加到執行的節點、執行執行中節點的函數,等等。

在以下部分中,我們會探討 service_discovery 專案的發佈建置。重點在於工具的現實世界使用方式,而非建構和執行發佈的低階細節。

建置開發發佈

service_discovery 中要查看的第一個地方是 rebar.configrelx 部分。

{relx, [{release, {service_discovery, {git, long}},
         [service_discovery_postgres,
          service_discovery,
          service_discovery_http,
          service_discovery_grpc,
          recon]},

        {sys_config, "./config/dev_sys.config"},
        {vm_args, "./config/dev_vm.args"},

        {dev_mode, true},
        {include_erts, false},

        {extended_start_script, true},

        {overlay, [{copy, "apps/service_discovery_postgres/priv/migrations/*", "sql/"}]}]}.

relx 組態清單中的 release 組包含了發佈名稱、版本以及發佈中包含的應用程式。這裡的版本為 {git, long},表示要 relx 使用當前提交的完整 git sha 參考作為發佈版本。發佈將開機的應用程式包括 Postgres 儲存後端、主要服務介面的應用程式、DNS 設定、HTTP 和 grpc 前端,以及用於檢查生產節點的工具 recon

在此處,應用程式列出的順序很重要。建構啟動指令碼時,會針對所有應用程式,依據其相依性進行穩定排序,以決定啟動順序。但是,如果相依性順序不足以做出決定時,系統將維持清單中定義的順序。每個應用程式service_discovery_postgresservice_discovery_httpservice_discovery_grpc 都相依於 service_discovery,因此會先啟動後者。接下來會啟動 service_discovery_postgres,因為它最先在清單中。這很重要,因為我們需要先準備儲存後端,才能使用 HTTP 和 grpc 服務。

rebar3 釋出會執行 relx,其設定根據 rebar.config 中的 relx 區段和 Rebar3 專案結構,讓 relx 能夠在建構釋出時,找到必要的應用程式。

$ rebar3 release
===> Verifying dependencies...
===> Compiling service_discovery_storage
===> Compiling service_discovery
===> Compiling service_discovery_http
===> Compiling service_discovery_grpc
===> Compiling service_discovery_postgres
===> Starting relx build process ...
===> Resolving OTP Applications from directories:
          /app/src/_build/default/lib
          /app/src/apps
          /root/.cache/erls/otps/OTP-22.1/dist/lib/erlang/lib
          /app/src/_build/default/rel
===> Resolved service_discovery-c9e1c805d57a78d9eb18af1124962960abe38e70
===> Dev mode enabled, release will be symlinked
===> release successfully created!

在輸出中看見的第一個 relx 步驟是「擷取 OTP 應用程式」,接著列出會用於搜尋建置應用程式的目錄清單。針對 relx 釋出的設定中的每個應用程式,在此範例中為 service_discovery_postgresservice_discoveryservice_discovery_httpservice_discovery_grpcreconrelx 會找到建置應用程式的目錄,然後對其 .app 檔案中列出的任何應用程式執行相同的動作。

注意

應用程式 sasl 沒有包含在 relx 組態的應用程式清單中。在 Rebar3 範本中,它預設包含在清單中。這是因為 sasl 是特定釋出作業所需的應用程式。sasl 應用程式包含 release_handler,此應用程式提供執行釋出升級和降級的功能。由於我們在此著重於建立會直接取代的容器,而不是執行在線升級,因此不需包含 sasl

由於此釋出是使用 {dev_mode, true} 建置,因此會在釋出的 lib 目錄中建立符號連結,這些連結會指向每個應用程式,而不是複製應用程式

$ ls -l _build/default/rel/service_discovery/lib
lrwxrwxrwx ... service_discovery-c9e1c80 -> .../_build/default/lib/service_discovery

執行時期設定檔 dev_sys.configdev_vm.args 也是如此

$ ls -l _build/default/rel/service_discovery/releases/c9e1c805d57a78d9eb18af1124962960abe38e70
lrwxrwxrwx [...] sys.config -> [...]/config/dev_sys.config
lrwxrwxrwx [...] vm.args -> [...]/config/dev_vm.args

這樣我們就能在執行釋出進行在地端測試時,加快回饋迴圈的速度。只要停止並重新啟動釋出,就會擷取所有已修改的 beam 檔案或設定檔,不需要執行 rebar3 release

警告

在 Windows 中,relx 的 dev_mode 不一定會運作,但它會改為切換至複製動作。

開發版本的完整檔案系統樹狀圖如下

_build/default/rel/service_discovery/
├── bin/
│   ├── install_upgrade.escript
│   ├── nodetool
│   ├── no_dot_erlang.boot
│   ├── service_discovery
│   ├── service_discovery-c9e1c805d57a78d9eb18af1124962960abe38e70
│   └── start_clean.boot
├── lib/
│   ├── acceptor_pool-1.0.0 -> /app/src/_build/default/lib/acceptor_pool
│   ├── base32-0.1.0 -> /app/src/_build/default/lib/base32
│   ├── chatterbox-0.9.1 -> /app/src/_build/default/lib/chatterbox
│   ├── dns-0.1.0 -> /app/src/_build/default/lib/dns
│   ├── elli-3.2.0 -> /app/src/_build/default/lib/elli
│   ├── erldns-1.0.0 -> /app/src/_build/default/lib/erldns
│   ├── gproc-0.8.0 -> /app/src/_build/default/lib/gproc
│   ├── grpcbox-0.11.0 -> /app/src/_build/default/lib/grpcbox
│   ├── hpack-0.2.3 -> /app/src/_build/default/lib/hpack
│   ├── iso8601-1.3.1 -> /app/src/_build/default/lib/iso8601
│   ├── jsx-2.10.0 -> /app/src/_build/default/lib/jsx
│   ├── recon-2.4.0 -> /app/src/_build/default/lib/recon
│   ├── service_discovery-c9e1c80 -> /app/src/_build/default/lib/service_discovery
│   ├── service_discovery_grpc-c9e1c80 -> /app/src/_build/default/lib/service_discovery_grpc
│   ├── service_discovery_http-c9e1c80 -> /app/src/_build/default/lib/service_discovery_http
│   └── service_discovery_storage-c9e1c80 -> /app/src/_build/default/lib/service_discovery_storage
├── releases/
│   ├── c9e1c805d57a78d9eb18af1124962960abe38e70
│   │   ├── no_dot_erlang.boot
│   │   ├── service_discovery.boot
│   │   ├── service_discovery.rel
│   │   ├── service_discovery.script
│   │   ├── start_clean.boot
│   │   ├── sys.config.src -> /app/src/config/sys.config.src
│   │   └── vm.args.src -> /app/src/config/vm.args.src
│   ├── RELEASES
│   └── start_erl.data
└── sql
    ├── V1__Create_services_endpoints_table.sql
    └── V2__Add_updated_at_trigger.sql

最後一個目錄,sql,是由一個指令碼產生,overlay{copy, "apps/service_discovery_postgres/priv/migrations/*", "sql/"}。一個overlay會告訴relx有檔案要加入,這些檔案並未包含在應用程式和開機檔案的規範版本結構中。它會建立目錄、用mustache進行基本範本製作,以及複製檔案。在這個例子中,我們只需要複製service_discovery_postgres應用程式的遷移至版本sql的最高層目錄。遷移本來就應該包含在其中,因為它們อยู่ใน版本的應用程式的priv,但由於我們打算一次只有一個版本可用(請參閱資訊方塊瞭解更多資訊),所以這可以簡化我們稍後就會看到的遷移指令碼,以將其保留在一個已知的頂層目錄中。

資訊

OTP 版本結構支援擁有同一個版本的多個版本。它們共享同一個lib目錄,但可以有各應用程式的不同版本,甚至是不同的erts。這就是版本目錄有一個版本數字目錄並使用檔案RELEASES的原因。在這種環境下,將 SQL 檔案放在各版本共享的目錄中可能會產生問題。但由於我們專注於建立獨立版本,該版本將與容器中的任何其他版本分開執行,因此我們可以假設只有一個版本。

由於此版本使用 Postgres 儲存後端,因此啟動版本前,資料庫需要執行且可以存取。在專案的最高層目錄執行 Docker Compose 會建立資料庫並執行遷移

$ docker-compose up

提示

您不需要啟動全部功能,就能從版本中取得 Erlang shell。每個用 Rebar3 建立的版本都會附帶一個名為start_clean的開機指令碼,它只會啟動kernelstdlib應用程式。可以使用指令console_clean執行它,這對除錯很有幫助。

若要將開發版本開機至一個互動 Erlang shell,請使用指令碼console執行擴充開機指令碼

$ _build/default/rel/service_discovery/bin/service_discovery console
Erlang/OTP 22 [erts-10.5] [source] [64-bit] [smp:1:1] [ds:1:1:10] [async-threads:30] [hipe]

(service_discovery@localhost)1>

執行service_discovery後,可以使用curl存取 HTTP 介面。下列指令會建立一個服務service1、透過列出所有服務來驗證它已建立、在 IP 為127.0.0.3的服務中註冊一個端點,並在 8000 埠上新增一個命名埠http

$ curl -v -XPUT https://127.0.0.1:3000/service \
    -d '{"name": "service1", "attributes": {"attr-1": "value-1"}}'
$ curl -v -XGET https://127.0.0.1:3000/services
[{"attributes":{"attr-1":"value-1"},"name":"service1"}]
$ curl -v -XPUT https://127.0.0.1:3000/service/service1/register \
    -d '{"ip": "127.0.0.3", "port": 8000, "port_name": "http", "tags": []}'
$ curl -v -XPUT https://127.0.0.1:3000/service/service1/ports \
    -d '{"http": {"protocol": "tcp", "port": 8000}}'

service_discoveryDNS 伺服器執行在 8053 埠上,使用dig可查看service1在正確的 IP 上註冊為端點,以及服務(SRV)DNS 查詢會傳回服務的埠和 DNS 名稱。

$ dig -p8053 @127.0.0.1 A service1
;; ANSWER SECTION:
service1.		3600	IN	A	127.0.0.3
$ dig -p8053 @127.0.0.1 SRV _http._tcp.service1.svc.cluster.local
;; ANSWER SECTION:
_http._tcp.service1.svc.cluster.local. 3600 IN SRV 1 1 8000 service1.svc.cluster.local.

建立生產版本

準備要部署到生產環境的版本,需要與在地端開發期間最適用的不同選項。Rebar3 設定檔讓我們可以取代和新增relx設定。此設定檔通常命名為prod

{profiles, [{prod, [{relx, [{sys_config_src, "./config/sys.config.src"},
                            {vm_args_src, "./config/vm.args.src"},
                            {dev_mode, false},
                            {include_erts, true},
                            {include_src, false},
                            {debug_info, strip}]}]
            }]}.

我們在prod設定檔中有兩個逾寫設定值。dev_mode設為false,因此所有內容都會複製到版本目錄中,我們無法使用符號連結連結到另一台電腦的build_目錄中,而製作版本應該是要變成專案中的永恆快照。include_erts會複製 Erlang 執行階段和 Erlang/OTP 應用程式,這些應用程式會依賴於版本放入版本目錄中,並設定開機腳本來指向這個執行階段的副本。

新增到設定檔的條目會將include_src設為falsedebug_info設為strip,還有設定檔的src版本。在製作版本中執行這個版本不需要原始碼,所以我們將include_src設為 false,從最終版本中移除它來節省空間。將 beam 檔案的 debug 資訊移除,將debug_info設為strip,可以節省更多空間。debug 資訊由偵錯程式、xrefcover等工具使用,但這些工具不會用於製作版本中,而且除非明確包含,否則甚至無法使用。

sys_config_srcvm_args_src比我們在預設設定檔中的條目sys_configvm_args優先,這兩個條目會在下一段落執行階段設定檔中討論更多。如果使用者並非有意要這麼做,在製作版本時會印出一個警告,但我們之所以這麼做就是為了要讓人可以忽略警告。

開啟製作版本的設定檔後會將產生的手稿寫入設定檔目錄_build/prod/

$ rebar3 as prod release
===> Verifying dependencies...
===> Compiling service_discovery_storage
===> Compiling service_discovery
===> Compiling service_discovery_http
===> Compiling service_discovery_grpc
===> Compiling service_discovery_postgres
===> Starting relx build process ...
===> Resolving OTP Applications from directories:
          /app/src/_build/prod/lib
          /app/src/apps
          /root/.cache/erls/otps/OTP-22.1/dist/lib/erlang/lib
===> Resolved service_discovery-c9e1c805d57a78d9eb18af1124962960abe38e70
===> Both vm_args_src and vm_args are set, vm_args will be ignored
===> Both sys_config_src and sys_config are set, sys_config will be ignored
===> Including Erts from /root/.cache/erls/otps/OTP-22.1/dist/lib/erlang
===> release successfully created!

檢視新的prod設定檔的版本目錄目錄結構會看到

_build/prod/rel/service_discovery
├── bin
│   ├── install_upgrade.escript
│   ├── nodetool
│   ├── no_dot_erlang.boot
│   ├── service_discovery
│   ├── service_discovery-c9e1c805d57a78d9eb18af1124962960abe38e70
│   └── start_clean.boot
├── erts-10.5
│   ├── bin
│   ├── doc
│   ├── include
│   ├── lib
│   └── man
├── lib
│   ├── acceptor_pool-1.0.0
│   ├── asn1-5.0.9
│   ├── base32-0.1.0
│   ├── chatterbox-0.9.1
│   ├── crypto-4.6
│   ├── dns-0.1.0
│   ├── elli-3.2.0
│   ├── erldns-1.0.0
│   ├── gproc-0.8.0
│   ├── grpcbox-0.11.0
│   ├── hpack-0.2.3
│   ├── inets-7.0.8
│   ├── iso8601-1.3.1
│   ├── jsx-2.10.0
│   ├── kernel-6.5
│   ├── mnesia-4.16
│   ├── public_key-1.6.7
│   ├── recon-2.4.0
│   ├── service_discovery-c9e1c80
│   ├── service_discovery_grpc-c9e1c80
│   ├── service_discovery_http-c9e1c80
│   ├── service_discovery_storage-c9e1c80
│   ├── ssl-9.3.1
│   └── stdlib-3.10
├── releases
│   ├── c9e1c805d57a78d9eb18af1124962960abe38e70
│   │   ├── no_dot_erlang.boot
│   │   ├── service_discovery.boot
│   │   ├── service_discovery.rel
│   │   ├── service_discovery.script
│   │   ├── start_clean.boot
│   │   ├── sys.config.src
│   │   └── vm.args.src
│   ├── RELEASES
│   └── start_erl.data
└── sql
    ├── V1__Create_services_endpoints_table.sql
    └── V2__Add_updated_at_trigger.sql

lib下沒有符號連結,而且像stdlib-3.10這種 OTP 應用程式會包含在內。這個目錄結構最上方是erts-10.5,這個資料夾包含 Erlang 執行階段,也就是bin/beam.smp,還有執行並與版本互動所需的執行檔,例如erlexecerlescript

要在版本中建置目標系統,我們在prod設定檔中執行tar指令

$ rebar3 as prod tar

現在我們有一個 tarball _build/prod/rel/service_discovery/service_discovery-c9e1c805d57a78d9eb18af1124962960abe38e70.tar.gz,這個 tarball 可以複製到任何相容的主機、解壓縮然後執行。但在執行之前,我們需要了解如何設定sys.config.srcvm.args.src,才能在開機時載入版本。

執行階段設定檔

service_discovery 項目中,我們有兩個檔案會在生產版本中包含在 config/ 目錄下的組態:vm.args.srcsys.config.src。這些檔案會在執行時間根據環境變數作為要填入的範本。之所以有兩個獨立的檔案,是因為執行版本會衍生兩層的組態。首先,會有基礎 Erlang 虛擬機器設定。這些值必須在虛擬機器啟動前設定,因此無法成為類似 sys.config 的術語檔的一部分,因為 sys.config 需要一個正在執行的虛擬機器來讀取和解析 Erlang 術語檔。相反地,虛擬機器參數會直接傳給 erlerl 是個用來啟動版本的指令。為了簡化這個步驟,erl 指令有 -args_file 參數,可允許從一般文字檔讀取命令列參數。這個檔案通常被命名為 vm.args

第二層是構成版本的 Erlang 應用程式組態。這會透過透過 -config 參數傳遞到 erl 的檔案來達成。這個檔案是一個 2 元組清單,第一個元素是希望設定環境的應用程式名稱,而第二個元素是希望在環境中設定的金鑰-值對清單。

當然,靜態檔案可能會造成很大的限制,而透過作業系統環境變數來設定組態也已變得普遍。為了提供彈性並支援環境變數組態,在使用 Rebar3 時所產生的版本啟動指令碼可將 ${FOO} 形式的變數替換成在目前環境中找到的值。如果檔案的副檔名為 .src,則會自動執行這個動作。

relx 組態中,我們使用 vm_args_srcsys_config_src 來包含檔案及標示它們是範本,這是有必要的,這樣版本建置就不會嘗試驗證 sys.config 是否是 Erlang 術語的適當清單,舉例來說 {port, ${PORT}} 不是適當清單

{relx, [...
        {sys_config_src, "config/sys.config.src"},
        {vm_args_src, "config/vm.args.src"},
        ...
       ]}.

在 Docker 及 Kubernetes 章節中,我們會討論為何需要設定 sbwt,但目前我們只關心如何在 vm.args.src 中執行這項動作

+sbwt ${SBWT}

sys.config.src 中,我們也會將 logger 層級設為變數,這樣我們就可以啟用 debuginfo 層級的記錄,以便在調查已佈署服務時取得更多細節

{kernel, [{logger_level, ${LOGGER_LEVEL}}]}

現在,在執行版本時,我們必須設定這些變數,否則版本會無法啟動。

$ DB_HOST=localhost LOGGER_LEVEL=debug SBWT=none \
  _build/prod/rel/service_discovery/bin/service_discovery console
Erlang/OTP 22 [erts-10.5] [source] [64-bit] [smp:1:1] [ds:1:1:10] [async-threads:30] [hipe]

(service_discovery@localhost)1>

在沒有設定必要的環境變數的情況下執行時產生的錯誤可能會令人困惑。目前並未執行驗證來檢查組態檔中使用的所有環境變數是否已設定,然後列印出哪些遺失了。遺失的變數會取而代之的是空字串。如果變數用於字串中,例如:"${DB_HOST}" 應用程式將會啟動,但是無法連線到資料庫。當遺失的變數建立了一個 sys.config 而無法剖析時,例如:${LOGGER_LEVEL},將會產生語法錯誤

$ DB_HOST=localhost SBWT=none \
  _build/prod/rel/service_discovery/bin/service_discovery console
{"could not start kernel pid",application_controller,"error in config file \"/app/src/_build/prod/rel/service_discovery/releases/71e109d8f34ef5e5ccfcd666e0d9e544836044f1/sys.config\" (48): syntax error before: ','"}
could not start kernel pid (application_controller) (error in config file "/home/tristan/Devel/service_discovery/_build/prod/rel/service_discovery/releases/71e109d8f34ef5e5ccfcd666e0d9e544836044f1/sys

Crash dump is being written to: erl_crash.dump...done

vm.args 中的值遺失時,您可能會看到有關傳遞給標幟的引數的錯誤。對於 +sbwt ${SBWT} 會導致嘗試使用下一行做為傳遞給 +sbwt 的引數,此情況下會是 +C multi_time_warp 產生開機錯誤,表示 +C 不是一個有效的

$ DB_HOST=localhost LOGGER_LEVEL=debug \
  _build/prod/rel/service_discovery/bin/service_discovery console
bad scheduler busy wait threshold: +C
Usage: service_discovery [flags] [ -- [init_args] ]
The flags are:
...

並非所有來自遺失變數的失敗都是如此簡單,可能會導致非常奇怪的崩潰,或開機時不會崩潰,但會在依賴值內的應用程式中發生失敗。我們希望在未來的 Rebar3 版本中改進這一點,以啟用在啟動前執行檢查,該檢查會傳回清單列出遺失的環境變數並會失敗。但是目前如果看到奇怪的行為或無法理解的崩潰,檢查環境變數是一個良好的起點。

即將推出

為了了解我們將在下一章節中執行哪些動作,將 _build/prod/rel/service_discovery/service_discovery-c9e1c805d57a78d9eb18af1124962960abe38e70.tar.gz 複製到 /tmp/service_discovery,解壓縮並啟動節點。

$ export LOGGER_LEVEL=debug
$ export SBWT=none
$ export DB_HOST=localhost
$ mkdir /tmp/service_discovery
$ cp _build/prod/rel/service_discovery/service_discovery-c9e1c805d57a78d9eb18af1124962960abe38e70.tar.gz /tmp/service_discovery
$ cd /tmp/service_discovery
$ tar -xvf service_discovery-c9e1c805d57a78d9eb18af1124962960abe38e70.tar.gz && rm service_discovery-c9e1c805d57a78d9eb18af1124962960abe38e70.tar.gz
...
$ ls
bin  erts-10.5  lib  releases
$ bin/service_discovery console
...
(service_discovery@localhost)1>

console 下執行會提供互動式 Erlang shell。若要在沒有互動式 shell 的情況下執行釋出版本,請使用 foreground,我們會在下一章節示範如何於 Docker 容器中執行釋出版本。當釋出版本使用 foregroundconsole 執行時,開啟一個不同的終端機並使用引數 remote_console 嘗試相同的指令碼

$ export LOGGER_LEVEL=debug
$ export SBWT=none
$ export DB_HOST=localhost
$ bin/service_discovery remote_console
...
(service_discovery@localhost)1>

remote_console 使用 -remsh erl 標幟來將互動式 shell 連線到已執行的釋出版本。該指令知道要連線到哪一個執行的 Erlang 節點,根據與組態相同的配置,從 vm.args,設定名稱和 cookie 給當初執行的節點。這表示任何用於填充 vm.args 的環境變數,在啟動節點時,都必須設定相同,以便連結遠端主控台。