Docker Swarm上写一个聊天室应用chitchat
这篇文章主要介绍"Docker Swarm上写一个聊天室应用chitchat",在日常操作中,相信很多人在Docker Swarm上写一个聊天室应用chitchat问题上存在疑惑,小编查阅了各式资料,整理出简单好用的操作方法,希望对大家解答"Docker Swarm上写一个聊天室应用chitchat"的疑惑有所帮助!接下来,请跟着小编一起来学习吧!
Phoenix web应用和其他语言/框架实现的web应用相比最大的不同点在于,Phoenix应用能在有状态的情况下依然保持很好的横向扩展能力,这得益于其底层的Erlang OTP支持。为了能在集群中的各节点之间共享状态,各节点只需相互认识即可,并不需要单独开一个状态容器(如Redis),这也使得Phoenix应用的架构更为简单明了。Elixir 1.9更加入了对release的支持,也使得打包部署更加方便。但是,这些都仅限于传统的、预先知道集群容量和各节点IP的部署方式。如何能在Docker Swarm上,在不预先知道各节点IP的情况下部署Phoenix应用并做到动态扩容成了下一个挑战。今天就来尝试部署一下最典型的有状态web应用--基于WebSocket的聊天室。
写一个聊天室应用chitchat
因为不是重点所以不写了。如果你不会写,直接去GitHub上拉代码
Release准备
这里只做最简单的准备。
$ mix release.init
我们还需要在config/prod.secret.exs里加一行代码让我们release出来的包(artifact)知道要启动所有相关的application。
config :chitchat, ChitchatWeb.Endpoint, server: true
创建Docker镜像
在项目的根目录下创建Dockerfile,并加入以下内容:
FROM elixir:1.9.1-alpine as build# install build dependenciesRUN apk add --update git build-base nodejs npm yarn python# prepare build dirRUN mkdir /appWORKDIR /app# install hex + rebarRUN mix local.hex --force && \ mix local.rebar --force# set build ENVENV MIX_ENV=prod# install mix dependenciesCOPY mix.exs mix.lock ./COPY config configRUN mix deps.getRUN mix deps.compile# build assetsCOPY assets assetsRUN cd assets && npm install && npm run deployRUN mix phx.digest# build projectCOPY priv privCOPY lib libRUN mix compile# build releaseCOPY rel relRUN mix release# prepare release imageFROM alpine:3.9 AS appRUN apk add --update bash opensslRUN mkdir /appWORKDIR /appCOPY --from=build /app/_build/prod/rel/chitchat ./RUN chown -R nobody: /appUSER nobodyENV HOME=/app
这是从Phoenix官方文档里直接复制过来的,除了改了一下应用名称和在apk add
里添加了npm以及把COPY rel rel
放出来以外什么都没改。
这是一个multi-stage的Dockerfile,为了使最终生成的镜像尽可能小,我们把Elixir、Mix、node.js等运行时不需要的东西全都留在了build阶段的镜像里,只把最终release出来的东西(包含Erlang运行时)放进了最终镜像。我这里构建出来的docker镜像约35MB。虽然现在已经能构建了,但我暂时不构建。
创建Swarm
为了图方便,我只做了单节点swarm:
$ docker swarm init
如果你手上有3台以上的电脑,你也可以做全尺寸swarm。这不是重点所以略过。如果你不知道怎么做,参考官方教程。
本地化Docker Registry
由于部署到swarm集群里的服务必须使用预先构建好的镜像(如果每个节点各自构建镜像又慢又耗资源),而实际生产环境下每个镜像可能会很大(上G),所以我们需要一个在内网里的Docker Registry来注册并在各个节点上共享镜像。
在任意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 PORTSx2eym27lc2b8 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 remoteiex> Node.connect(:"chitchat@10.0.0.3")
你会发现连不上。问题在哪儿?看看当前节点的名称是啥:
iex> Node.self():"nonode@nohost"
问题就在这儿。我们的节点没有名称!为了让每个节点有自己的名称,我们需要修改rel/env.sh.eex。
修改rel/env.sh.eex
放开下面两行:
export RELEASE_DISTRIBUTION=nameexport 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.exsconfig :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前面加上
,然后尝试连接这些节点。
重新部署一下,然后在某个replica上运行
$ ./bin/chitchat rpc "IO.inspect Node.list"
你会看到其他节点的名称,这表明所有节点都已连上了。
你还可以尝试扩张/缩水当前的服务(参考docker service scale
),杀掉某个容器(docker kill
)等操作,看看行为是否和预期一样。
到此,关于"Docker Swarm上写一个聊天室应用chitchat"的学习就结束了,希望能够解决大家的疑惑。理论与实践的搭配能更好的帮助大家学习,快去试试吧!若想继续学习更多相关知识,请继续关注网站,小编会继续努力为大家带来更多实用的文章!