Phoenix Web應用在Docker Swarm上的部署

Phoenix web應用和其餘語言/框架實現的web應用相比最大的不一樣點在於,Phoenix應用能在有狀態的狀況下依然保持很好的橫向擴展能力,這得益於其底層的Erlang OTP支持。爲了能在集羣中的各節點之間共享狀態,各節點只需相互認識便可,並不須要單獨開一個狀態容器(如Redis),這也使得Phoenix應用的架構更爲簡單明瞭。Elixir 1.9更加入了對release的支持,也使得打包部署更加方便。可是,這些都僅限於傳統的、預先知道集羣容量和各節點IP的部署方式。如何能在Docker Swarm上,在不預先知道各節點IP的狀況下部署Phoenix應用並作到動態擴容成了下一個挑戰。今天就來嘗試部署一下最典型的有狀態web應用——基於WebSocket的聊天室。node

寫一個聊天室應用chitchat

由於不是重點因此不寫了。若是你不會寫,直接去GitHub上拉代碼python

Release準備

這裏只作最簡單的準備。git

$ mix release.init

咱們還須要在config/prod.secret.exs里加一行代碼讓咱們release出來的包(artifact)知道要啓動全部相關的application。github

config :chitchat, ChitchatWeb.Endpoint, server: true

建立Docker鏡像

在項目的根目錄下建立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

爲了圖方便,我只作了單節點swarm:npm

$ docker swarm init

若是你手上有3臺以上的電腦,你也能夠作全尺寸swarm。這不是重點因此略過。若是你不知道怎麼作,參考官方教程瀏覽器

本地化Docker Registry

因爲部署到swarm集羣裏的服務必須使用預先構建好的鏡像(若是每一個節點各自構建鏡像又慢又耗資源),而實際生產環境下每一個鏡像可能會很大(上G),因此咱們須要一個在內網裏的Docker Registry來註冊並在各個節點上共享鏡像。bash

在任意manager節點上運行

$ docker service create --name registry -p 5000:5000 registry:2

這一句會在你的swarm裏建立一個名爲registry的服務,用的鏡像是Docker官方的registry:2,公開5000端口。它只有一個replica。

構建鏡像並推上Registry

運行命令

$ 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

在項目的根目錄下建立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。

修改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。

集成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)等操做,看看行爲是否和預期同樣。

至此,整個實驗成功結束。其他的問題(如何部署數據庫、如何作全局惟一進程等)留待下次再作實驗。

相關文章
相關標籤/搜索