Phoenix + WebSocket分布式部署验证


声明:本文转载自https://my.oschina.net/u/3390582/blog/1631370,转载目的在于传递更多信息,仅供学习交流之用。如有侵权行为,请联系我,我会及时删除。

Phoenix + WebSocket分布式部署实验

前言

对于无状态的web服务,要做分布式部署相对比较简单,很多时候只要架一个反向代理就行。但是对于有状态的web服务,尤其是包含WebSocket成分的web应用,要做分布式部署一直是一个麻烦。传统做法是搞一个中间层,例如Redis之类的做pubsub,但是这样做就不得不改动源码。Erlang/Elixir这种“面向并发编程”的语言在这方面会不会高人一筹?Phoenix框架是否已经支持WebSocket的横向扩展了呢?下面我们就来做个实验。

资源

你可以去https://gitee.com/aetherus/gossipy下载本文涉及的源码。

目标

不添加其他服务(如Redis、RabbitMQ等),不改动项目源码,仅通过添加/修改配置文件来达到WebSocket服务的横向扩展。

实验器材

  • Ubuntu 16.04或其衍生发行版(我用的是Elementary OS Loki)
  • Docker
  • Docker compose
  • Elixir开发/运行环境
  • 一个最基本的Phoenix聊天室,不含数据库,不含assets,不含brunch。

安装发布工具

Elixir社区目前比较推荐的发布工具是Distillery(蒸馏器),这次实验就用它。

安装只需要在项目根目录的mix.exs里添加如下内容就行

defp deps do   [     {:distillery, "~> 1.5", runtime: false}   #<--- 这一行   ] end 

这里的runtime: false表示distillery不会在web应用中用到,只在发布的时候用一下。
添加完后只需mix deps.get一下就行。

发布配置

首先让distillery生成一些最基本的发布配置:

$ mix release.init 

你会看到项目根目录下多了个rel目录,里面只有一个空的plugins目录和一个config.exs文件。这个文件的配置用来发布到单台服务器已经足够了,但是要做集群还是不太够,因为我们要让各台服务器上的Phoenix应用能连起来相互通信。为此,我们需要给每个运行的实例一个名称(namesname)。

为了达到这个目的,我们需要一个vm.args文件。这个文件记录了Erlang启动虚拟机时所需的命令行参数。但是这个文件长啥样?我们现release一个,让它自动生成一个vm.args文件再说。

$ MIX_ENV=prod mix release --env prod 

这里的MIX_ENV=prod是指“用Phoenix的prod环境的配置来运行发布任务”,这样做可以使项目的编译得到优化,比如去除debug信息等。而--env prod指的是“按rel/config.exs文件里:prod环境的配置去构建发布版”。这个prod和Phoenix的prod的意义完全不同,所以两个都不能少。

既然说到了rel/config.exs里定义的环境,就先看看它长什么样吧。

Path.join(["rel", "plugins", "*.exs"]) |> Path.wildcard() |> Enum.map(&Code.eval_file(&1))  use Mix.Releases.Config,     default_release: :default,     default_environment: Mix.env()  environment :dev do   set dev_mode: true   set include_erts: false   set cookie: :"<&9.`Eg/{6}.dwYyDOj>R6R]2IAK;5*~%JN(bKuIVEkr^0>jH;_iBy27k)4J1z=m" end  environment :prod do   set include_erts: true   set include_src: false   set cookie: :">S>1F/:xp$A~o[7UFp[@MgYVHJlShbJ.=~lI426<9VA,&RKs<RyUH8&kCn;F}zTQ" end  release :gossipy do   set version: current_version(:gossipy)   set applications: [     :runtime_tools   ] end 

这就是一个完整的rel/config.exs文件内容(去掉了注释)。我们可以看到里面有个environment :prod块,还有一个environment :dev块,这两个块定义了两种不同的构建策略。这里比较重要的是set include_erts: true|false这一项。erts是“Erlang RunTime System”的缩写,也就是整个Erlang运行环境。如果把这一项设置成true,则打出来的包里包含整个Erlang运行环境,于是你的目标服务器上就可以不用装Erlang和Elixir了。

上述命令运行完后,会生成_build/prod/rel目录及其下面所有的文件。在这里面找到vm.args文件(具体位置忘了),把它复制到项目根目录下的rel目录里,稍事修改:

# 删除下面这一行 # -name gossipy@127.0.0.1 # 加入下面这一行 -sname gossipy 

namesname的区别不多说了。因为到时候我们要部署到docker上去,用IP或全限定域名不方便,所以就用主机名。

改完vm.args之后,我们要让distillery认识这个改动过的vm.args。我们在rel/config.exs里加上一行:

environment :prod do   ...   set vm_args: "rel/vm.args" end 

除了这些,Distillery还要求在项目的config/prod.exs里加一些东西:

config :gossipy, GossipyWeb.Endpoint,   ...   check_origin: false,   server: true,   root: ".",   version: Application.spec(:gossipy, :vsn) 
  • check_origin: false只是做实验的时候图一时方便,正式上产品的时候千万不要加这一行。
  • server: true的意思是这是一个web server,所以要用Cowboy去启动,而不是直接从Application启动。
  • root: "."表示静态文件(CSS,JS之类)的根在哪儿。因为我们这次没有静态文件,所以不配也OK。
  • version是发布的版本号。它的值通过Application.spec(:gossipy, :vsn)获取,也就是mix.exs里那个版本号。

另外,我们需要在这个配置文件里列出所有的分布式节点:

config :kernel,   sync_nodes_optional: [:"gossipy@ws1", :"gossipy@ws2"],   sync_nodes_timeout: 10000 

sync_nodes_optional是指“如果在sync_nodes_timeout指定的时间范围内没有连上指定的节点,则忽略那个节点”。与之相对的还有一个sync_nodes_mandatory选项。

所有配置都准备好后,先清除掉上次构建的发布版,再重新构建一次:

$ MIX_ENV=prod mix release.clean $ MIX_ENV=prod mix release --env prod 

然后就可以准备部署了

创建Docker镜像

既然是部署到Docker,就要先创建一份Dockerfile,内容如下:

FROM ubuntu:xenial  EXPOSE 4000  ENV PORT=4000  RUN mkdir -p /www/gossipy && \     apt-get update && \     apt-get install -y libssl-dev  ADD ./_build/prod/rel/gossipy/releases/0.0.1/gossipy.tar.gz /www/gossipy  WORKDIR /www/gossipy  ENTRYPOINT ./bin/gossipy foreground 

因为发布包内的Erlang运行环境要求服务器的OS和Distillery运行时的OS尽可能一样,所以这里就用Ubuntu 16.04的服务器版。端口设为4000(你喜欢其他端口号也OK)。由于WebSocket需要crypto.so,所以先装一下libssl-dev,否则应用起不来。把打包出来的tar包扔进镜像(docker会替你自动解压),当docker启动的时候把这个服务启动起来就是了。

为了能简化命令行命令,再建一个docker-compose.yml

version: '3.2'  services:   ws1:     build: .     hostname: ws1     ports:       - 4001:4000    ws2:     build: .     hostname: ws2     ports:       - 4002:4000 

我定义了两个节点,分别将宿主的4001和4002端口NAT到了docker容器的4000端口。另外,这里显式声明了每个节点的主机名(hostname),方便和Phoenix应用对接。

一切OK后,docker-compose up

然后你就可以想办法搞两个WebSocket客户端(如果你不知道怎么搞的话,可以参考附录1),分别连接宿主服务器的4001和4002端口,加入同一个房间,然后你就能看见它们能对话了!

额外实验1. 杀节点

先杀掉ws2那个容器(端口4002)

$ docker-compose kill ws2 

结果当然是连在ws2上的WebSocket连接全部断开,而ws1上的连接依然正常工作。ws2的连接中断很正常。在实际项目中,我们不会把一个web服务分在多个端口号上,而是公用一个源(协议 + 域名 + 端口),这样只要客户端实现了合理的重连机制,很快就能和别的活着的服务器建立连接。

然后我们再把ws2重新启动起来

$ docker-compose start ws2 

重新建立和ws2的连接后,两台服务器上的连接又能正常通信了。

额外实验2. 添加节点

这次试的是在不重启现有服务器集群的前提下,向集群中添加服务器。

为此,我们先在docker-compose.yml中添加一个服务

  ws3:     build: .                        hostname: ws3                   ports:                            - 4003:4000 

然后修改一下config/prod.exs,把新的节点加进去

config :kernel,   sync_nodes_optional: [:"gossipy@ws1", :"gossipy@ws2", :"gossipy@ws3"],  #<---- 注意新加ws3   sync_nodes_timeout: 10000 

重新发布一下,并启动ws3容器

$ MIX_ENV=prod mix release.clean $ MIX_ENV=prod mix release --env prod $ docker-compose up -d ws3 

用浏览器测试相当成功!新加的节点马上就连上老节点, 老节点也立刻就认识新节点了。

结论

正如所料,Phoenix可以在不改动一行代码的情况下做到WebSocket的集群化。这就是Erlang/Elixir的特色之一——Location Transparency(位置透明)给我们带来的好处。单机运行代码和分布式运行代码完全一样!只是要用好这个位置透明,在没人手把手教你的情况下,你会尝试错误好几次。

附录1. 测试用HTML

<!doctype html> <html>    <head>     <meta charset="utf-8">     <title>Phoenix Channel Demo</title>        </head>    <body>      <pre id="messages"></pre>        <input id="shout-content">         <script>       window.onload = function () {            var wsPort = window.location.search.match(/\bport=(\d+)\b/)[1];         var messageBox = document.getElementById('messages');         var ws = new WebSocket('ws://localhost:' + wsPort + '/socket/websocket');            ws.onopen = function () {                  ws.send(JSON.stringify({                     topic: 'room:1',             event: 'phx_join',             payload: {},             ref: 0           }));         };          ws.onmessage = function (event) {           var data = JSON.parse(event.data);           if (data.event !== 'shout') return;           messageBox.innerHTML += data.payload.message + "\n";          }          document.getElementById('shout-content').onkeyup = function (event) {           if (event.which !== 13) return;            if (!event.target.value) return;           ws.send(JSON.stringify({                     topic: "room:1",             event: "shout",             payload: {message: event.target.value},             ref: 0           }));           event.target.value = '';         };       }     </script>   </body> </html> 

你可以用任何手段host它,使得浏览器能通过HTTP访问到它(用file://协议不行)。例如,你可以把它存入文件ws.html,然后用python -m SimpleHTTPServer来启动一个简易HTTP服务(默认端口号8000),然后用浏览器访问http://localhost:8000/ws.html?port=4001。这里的port参数指定连接到哪个WebSocket端口。

本文发表于2018年03月08日 22:45
(c)注:本文转载自https://my.oschina.net/u/3390582/blog/1631370,转载目的在于传递更多信息,并不代表本网赞同其观点和对其真实性负责。如有侵权行为,请联系我们,我们会及时删除.

阅读 2155 讨论 0 喜欢 0

抢先体验

扫码体验
趣味小程序
文字表情生成器

闪念胶囊

你要过得好哇,这样我才能恨你啊,你要是过得不好,我都不知道该恨你还是拥抱你啊。

直抵黄龙府,与诸君痛饮尔。

那时陪伴我的人啊,你们如今在何方。

不出意外的话,我们再也不会见了,祝你前程似锦。

这世界真好,吃野东西也要留出这条命来看看

快捷链接
网站地图
提交友链
Copyright © 2016 - 2021 Cion.
All Rights Reserved.
京ICP备2021004668号-1