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
)等操做,看看行为是否和预期同样。
至此,整个实验成功结束。其他的问题(如何部署数据库、如何作全局惟一进程等)留待下次再作实验。