Phoenix web應用和其餘語言/框架實現的web應用相比最大的不一樣點在於,Phoenix應用能在有狀態的狀況下依然保持很好的橫向擴展能力,這得益於其底層的Erlang OTP支持。爲了能在集羣中的各節點之間共享狀態,各節點只需相互認識便可,並不須要單獨開一個狀態容器(如Redis),這也使得Phoenix應用的架構更爲簡單明瞭。Elixir 1.9更加入了對release的支持,也使得打包部署更加方便。可是,這些都僅限於傳統的、預先知道集羣容量和各節點IP的部署方式。如何能在Docker Swarm上,在不預先知道各節點IP的狀況下部署Phoenix應用並作到動態擴容成了下一個挑戰。今天就來嘗試部署一下最典型的有狀態web應用——基於WebSocket的聊天室。node
由於不是重點因此不寫了。若是你不會寫,直接去GitHub上拉代碼python
這裏只作最簡單的準備。git
$ mix release.init
咱們還須要在config/prod.secret.exs里加一行代碼讓咱們release出來的包(artifact)知道要啓動全部相關的application。github
config :chitchat, ChitchatWeb.Endpoint, server: true
在項目的根目錄下建立Dockerfile,並加入如下內容:web
FROM elixir:1.9.1-alpine as build # install build dependencies RUN apk add --update git build-base nodejs npm yarn python # prepare build dir RUN mkdir /app WORKDIR /app # install hex + rebar RUN mix local.hex --force && \ mix local.rebar --force # set build ENV ENV MIX_ENV=prod # install mix dependencies COPY mix.exs mix.lock ./ COPY config config RUN mix deps.get RUN mix deps.compile # build assets COPY assets assets RUN cd assets && npm install && npm run deploy RUN mix phx.digest # build project COPY priv priv COPY lib lib RUN mix compile # build release COPY rel rel RUN mix release # prepare release image FROM alpine:3.9 AS app RUN apk add --update bash openssl RUN mkdir /app WORKDIR /app COPY --from=build /app/_build/prod/rel/chitchat ./ RUN chown -R nobody: /app USER nobody ENV HOME=/app
這是從Phoenix官方文檔裏直接複製過來的,除了改了一下應用名稱和在apk add
裏添加了npm外什麼都沒改。docker
這是一個multi-stage的Dockerfile,爲了使最終生成的鏡像儘量小,咱們把Elixir、Mix、node.js等運行時不須要的東西全都留在了build階段的鏡像裏,只把最終release出來的東西(包含Erlang運行時)放進了最終鏡像。我這裏構建出來的docker鏡像約35MB。雖然如今已經能構建了,但我暫時不構建。數據庫
爲了圖方便,我只作了單節點swarm:npm
$ docker swarm init
若是你手上有3臺以上的電腦,你也能夠作全尺寸swarm。這不是重點因此略過。若是你不知道怎麼作,參考官方教程。瀏覽器
因爲部署到swarm集羣裏的服務必須使用預先構建好的鏡像(若是每一個節點各自構建鏡像又慢又耗資源),而實際生產環境下每一個鏡像可能會很大(上G),因此咱們須要一個在內網裏的Docker Registry來註冊並在各個節點上共享鏡像。bash
在任意manager節點上運行
$ docker service create --name registry -p 5000:5000 registry:2
這一句會在你的swarm裏建立一個名爲registry的服務,用的鏡像是Docker官方的registry:2
,公開5000端口。它只有一個replica。
運行命令
$ docker build --tag 127.0.0.1:5000/chitchat:0.1.0 .
便可構建出鏡像。版本號最好和mix.exs裏的保持一致。注意,Docker Registry貌似不會覆蓋已有鏡像(待考證),因此版本號最好不要用latest。127.0.0.1:5000
是registry的IP地址和端口號,根據你的swarm的實際狀況改之。構建完後運行命令
$ docker push 127.0.0.1:5000/chitchat:0.1.0
就能將這個鏡像推到本地的registry上了。
在項目的根目錄下建立docker-compose.yml,並添加下列內容:
version: '3.7' services: app: image: 127.0.0.1:5000/chitchat:0.1.0 ports: - 80:4000 entrypoint: ./bin/chitchat start deploy: mode: replicated replicas: 3
除了deploy
項以外,這能夠算是最簡單的docker-compose配置文件了。先跑跑看
$ docker-compose up
它應該能直接跑起來(雖然會有警告說deploy
項無效),訪問80端口、鏈接ws應該都沒問題。
接着咱們嘗試部署到swarm上(單節點的同窗記得把剛纔的試運行關掉哦):
$ docker stack deploy -c ./docker-compose.yml chitchat
確認服務都起來了
$ docker stack services chitchat
應該看到以下內容:
ID NAME MODE REPLICAS IMAGE PORTS x2eym27lc2b8 chitchat_app replicated 3/3 127.0.0.1:5000/chitchat:0.1.0 *:80->4000/tcp
若是看到REPLICAS是3/3,說明部署成功,若是一直是0/3,則檢查你的代碼有沒有問題。
爲了接下來的調試,先跟蹤一下日誌:
$ docker service logs -f chitchat_app
而後打開兩個瀏覽器窗口/標籤,訪問一下 http://127.0.0.1/rooms/1 ,看一下日誌確保ws鏈接到了不一樣的replica上,若是連在了同一個上面,則刷新其中一個窗口,直到它連到了不一樣的replica爲止。發一條消息試試,你會發現 另外一個窗口收不到消息!
問題出在哪兒了?問題出在各個節點上的epmd(Erlang Process Manager)各自爲政,沒有鏈接到一塊兒。因此下一步就是想辦法把它們連起來。
咱們知道Elixir有一個函數Node.connect/1
能夠鏈接到其餘節點,只要它們有相同的cookie。問題在於,這種鏈接方式須要預先知道對方的IP或域名或主機名。可是在一個容器編排系統(container orchestration system)裏,容器的IP、域名和主機名都是動態分配的,尤爲是在容器宕掉重啓後,它的IP、域名和主機名極可能會改變。在這種動態的集羣裏,怎麼才能讓容器找到本身的兄弟呢?
思路是利用Docker的基於DNS的服務發現機制。在Docker Swarm裏,每一個服務都帶有一個服務發現用的域名,它是tasks.<服務名>
,在咱們的這套配置裏,它是tasks.chitchat_app
。若是你在任意一個replica容器裏運行nslookup tasks.chitchat_app
,你會看到全部replica的IP地址。有了IP地址,接下來只要知道節點的基本名稱(節點名稱@前面的部分)就好了。這個名稱很容易找,由於Elixir的release啓動時,環境變量$RELEASE_NAME
已經設好了這個名稱。
看起來不錯,先試一下。讓咱們先登上1號容器(把那個xxx換成實際值,其實只須要敲Tab就好了):
$ docker exec -it chitchat_app.1.xxx sh
得到其餘容器的IP地址:
$ nslookup tasks.chitchat_app
而後attach到正在運行的chitchat進程,並嘗試鏈接其餘節點(假定它的IP是10.0.0.3):
$ ./bin/chitchat remote iex> Node.connect(:"chitchat@10.0.0.3")
你會發現連不上。問題在哪兒?看看當前節點的名稱是啥:
iex> Node.self() :"nonode@nohost"
問題就在這兒。咱們的節點沒有名稱!爲了讓每一個節點有本身的名稱,咱們須要修改rel/env.sh.eex。
放開下面兩行:
export RELEASE_DISTRIBUTION=name export RELEASE_NODE="<%= @release.name %>@127.0.0.1"
這個文件用於生成env.sh,而env.sh會在每次應用啓動的時候運行,用來設置環境變量。
還有一個問題,怎麼把127.0.0.1
替換成真正的容器的IP?若是你在某個容器裏運行hostname -i
,你會獲得當前的IP(比較有意思的是,若是你在本身的PC上運行這句命令,你只能拿到127.0.1.1
)。因此咱們只要把RELEASE_NODE那一行改爲
export RELEASE_NODE="<%= @release.name %>@$(hostname -i)"
就一切OK了。順帶一提,rel/env.bat.eex能夠不改,由於咱們的容器跑的不是Windows而是Alpine Linux。
從新部署一下,再嘗試一下鏈接其餘節點,能夠看到此次就能連上了。
下一個問題就是怎麼讓它自動連,並且週期性地反覆連。這裏我用了一個第三方庫Peerage。
安裝方式請自行看官網。我只將個人配置貼出來:
# config/prod.exs config :peerage, via: Peerage.Via.Dns, dns_name: "tasks.chitchat_app", app_name: {:system, "RELEASE_NAME"}
這裏的dns_name
就是Peerage去訪問的DNS域名。而app_name
則是節點名稱@前面的部分。{:system, "RELEASE_NAME"}
告訴Peerage這個名稱要去環境變量$RELEASE_NAME
裏找。Peerage會週期性地訪問DNS獲取IP,並在每一個IP前面加上<app_name>@
,而後嘗試鏈接這些節點。
從新部署一下,而後在某個replica上運行
$ ./bin/chitchat rpc "IO.inspect Node.list"
你會看到其餘節點的名稱,這代表全部節點都已連上了。
你還能夠嘗試擴張/縮水當前的服務(參考docker service scale
),殺掉某個容器(docker kill
)等操做,看看行爲是否和預期同樣。
至此,整個實驗成功結束。其他的問題(如何部署數據庫、如何作全局惟一進程等)留待下次再作實驗。