距离 1.2 发布已经有一年多,而 exlirconf 2016 McCord 宣布 1.3 的特性也已过去半年,phoenix 1.3 依旧犹抱琵琶半遮面,迟迟不肯现身。几天前,1.3 RC.0 悄然发布,我们终于可以一睹她的芳容。
引子
因为程序人生的读者大多不是 elixir / phoenix 的用户,所以在这里小小普及一下。elixir 是在 erlang VM 上发布的一门语法类似 ruby,能力完全继承 erlang,并支持 metaprogramming 的函数式编程语言。erlang VM 下以 actor model(请自行 wiki 之)为基础的 concurrency model,加之 pattern matching 的强大能力,辅以 metaprogramming 的上帝视角,让 elixir 充满独特的魅力。而 phoenix,是 elixir 语言下的一个脱胎于 rails 的框架,可以帮助我们快速打造 webapp。
phoenix 相对于 rails,根本性的颠覆有二:
1) 基于 erlang VM 的 concurrency 能力。你看不见的很多地方,都使用大量的 process,或者 process pool 来提升并发能力。很多时候,phoenix 的 performance 是 rails 的 5-10 倍。phoenix 的作者 McCord 做了一个实验,在单机上成功实现了 2M websocket connection。
2) realtime web。phoenix 大大简化了开发高性能 realtime app 的难度,通过抽象出 channel,让 join / leave / broadcast / presence 这些事情处理起来非常简单 —— 这让一个普通的工程师也可能写出非常 scalable 的 realtime app,比如一个支撑百万级用户的聊天软件。
以下是一篇文章 (https://hashrocket.com/blog/posts/websocket-shootout) 做的评测,测试方法是每个 websocket 收到 message 后 broadcast 给所有其他 websocket,完成后发送状态给 sender。在 95th percentile response time < 500ms 的情况下,看能支持多少 websocket:
我们可以看到,phoenix 的能力(大概 24,000 active websockets),落后于 C / clojure,和 go 并驾齐驱。
不过这个评测是非常不公平的 —— 在这里,phoenix 和 rails 都是 full-fledged framework,而其他都是直接使用语言的 websocket 库。这种对比就好像做网络性能测试,拿 UDP 和 TCP 对比,然后得出 UDP performance 要远好于 TCP 一样滑稽。考虑到 phoenix 在 websocket 基础上抽象出了 channel,每个 websocket connection,都是一对 process(一个处理网络层,一个处理 channel 层),并且从 connection 到 dispatch,都完整的走了 framework 的整个流程,达到这样一个效率还是相当惊人的。
目录结构的变迁
回到正题。phoenix 既然脱胎于 rails,一颦一笑都在模仿先祖。model,controller,view,template 一个都不少,scaffolding 出来的目录结构都异曲同工。这带来很多问题。其中最重要的,也是最根本的问题是:我们究竟在做一个包含了 web interface 的系统,还是在做一个以 web 为中心的 app?
这是每个 web app 在成长过程中不得不面临的问题。我在 rails, django, phoenix,你们错了 一文中提到:
说句不太好听的话,rails 等 framework 很容易引导人们走向一个 web 前端为中心的歧路。这里所说的「前端」,是指后端的前端。我们应该根据需求,先把业务模型构建出来,各个服务构建妥当后,再使用 rails 等打造前端。我们可能需要一个面向用户的前端,可能还要面向管理员的前端,每个独立的服务可能也需要它们各自的管理前端,我们还要有统计分析的前端,用户行为分析的前端等等。这些所有的前端基本都没有所谓的 model,因为数据的存储在各个服务中解决了。
我们看 phoenix 1.2 的目录结构:
这是典型的以 web 为中心的处理方法。你的数据模型,你的各种业务逻辑,似乎就是奔着一个 web interface 去的,虽然能很快搭建出一个 app,但从长远发展来看,有诸多问题。当然我们随着系统的发展,把业务逻辑和数据模型抽取出来,放在 lib 下,甚至,用 elixir / erlang 惯有的方式,将它们包装成一个个独立的 app,然而,scaffolding 出来的目录结构还是会深深地影响和制约着你的代码结构。起初,你会往 web/models 里塞 data model,往 web/controllers 里塞各种逻辑,慢慢地,你的代码就会变成这样的状态:处理业务的逻辑和处理 web 的逻辑揉在了一起,不同 model 间的逻辑揉在了一起,由此 controller 要了解很多 model 的细节,才能处理得当:
在这样的代码里,我们看不清系统各部分的边界在哪里。新的代码的插入是那样的顺理成章,以至于一切良好的设计都随着边界的模糊而变得混乱不堪。理想的状态是这样:
业务和 web 分开,Blog
看上去更像是一个 service,一个 web controller 并不需要关心细节(只要知道接口)的 service。
从上面的目录结构中演化出这样的代码并非易事 —— 新的代码放哪,目录如何设置,怎么命名,都是学问。Conway's laow告诉我们:
organizations which design systems ... are constrained to produce designs which are copies of the communication structures of these organizations
换句话说,一个公司的技术架构和设计受到该公司的组织架构的影响。同样的,Tyr's law 告诉我们:
一个系统的软件架构和设计和这个系统的目录结构非常相关。
在 phoenix 1.3 中,最大的变化就是目录结构的变化。我们欣喜地看到,models 不再隶属于 web,甚至,models 都不单独存在了,而 web,只是作为 app 的一个附庸而存在。由此,web 层被狠狠地削薄了,我们做一个系统不再是从 model 出发,在 controller 里构建逻辑,然后在 view 中呈现;相反,我们开始考虑如何打造 service,如何提供 internal 的 API,然后在这些 API 的基础上,提供 web interface。
由此,我们可以打造逻辑更为清晰的系统:
这样的目录结构,一眼望去,我们就大概知道系统提供什么样的服务,各个服务的边界在哪里:
对 unbrella project 的支持
在 elixir 中,umbrella project 是我的最爱。我不但喜欢把服务通过目录来划分势力范围,更钟情将它们构造成不同的 app 来进一步在运行时界定它们的边界。application 是 erlang VM 里一个非常重要的概念,这在其他 VM,其他语言中都不曾出现。一个 erlang VM,你可以将其看做是一个操作系统,这个操作系统里运行着很多各司其职的 application,每个 application 管理着它们各自的 process。在 rails 里,logger 是一个模块,db connector 是一个模块,它们运行在当前代码所在的上下文中。而 elixir / erlang 中,logger 是一个 app,db connector 是一个 app,当你要记录日志时,实际上是发一个 message 给 logger app,请它来处理 log,log 的最终写入是一个完全不同的上下文。这种在运行时把系统划分成不同 app 来管理的方式,我非常非常喜欢。它让系统的管理变得简单,边界清晰,解耦变得容易,系统的脉络一路了然。
在 phoenix 1.2 之前的版本,我使用 phoenix 的一个方式是先创建一个 umbrella project,然后在里面再创建只有 controller 和 view 的 phoenix app,这有些别扭;phoenix 1.3 中,我们终于可以直接使用 phoenix 来创建 umbrella project 了:
这让我在 rails, django, phoenix,你们错了 一文中提到的例子,从结构上打造起来方便很多:
以上种种,解耦经验丰富的工程师也许不屑一顾;但它的确为经验不那么丰富的工程师,从结构上指出了一条明路,尤其是很多直接从 rails 转 phoenix,对 elixir / erlang VM 还 一知半解的工程师。而 从结构上给出正确的方向,往往是 framework 的最大贡献。很欣喜,phoenix 1.3 终于迈出了这一步。
当然,这样的步子迈起来很痛,容易扯着蛋。基于 phoenix 的很多优秀的第三方库,一下子变得都不好用起来。写起代码,很难直接使用已有的架构在 phoenix 1.2 之上的 lib,于是掣肘丛生,只能踯躅前行 —— 而且,在可预见的几个月内,这状况不太会有太多的改变。然而这种痛,是一个架构逐渐成熟 —— 走出全盘借鉴别人的路子,结合语言的特性,形成自己独特思路的必经之路。
有意思的是,我第一个大规模使用的框架,django,也是在 1.2 到 1.3 的升级中,完成了 function based view 到 class based view 的蜕变。莫非,这就是天道轮回?
(本文的代码和大部分截图出自:https://www.youtube.com/watch?v=tMO28ar0lW8。McCord 大神亲自揭秘 phoenix 1.3 的更新。这个视频非常值得观看)