这篇文章谈一谈最近火爆的 Elixir,同时说一下对编程语言选择的看法。同时作为 Erlang 发烧友,Elixir 不可不提。即使有了那么多编程语言 Elixir 也值得接触。本文主要分为以下四块内容,点击「阅读原文」查看完整文章!
- 现在开始接触 Elixir
- 对编程语言选择的一点看法
- Elixir 的元编程 (meta programming) 和 DSL
- 很多有用的链接
这里节选文章的第二部分,主要对Elixir的特性和生态进行了介绍。
对编程语言选择的一点看法
作为个语言发烧友,之前接触过 Java、Erlang、Scala、PHP、JavaScript、C#、C、Python、Ruby 等一大堆各种风格的编程语言。有人说,学那么多编程语言是想做”翻译”吗?其实事情并不那么简单。
不同的语言背后是风格截然不同的类库群、技术堆栈、生态和工具链。不同的语言针对了不同类型的问题。某些语言解决某些问题的成本会比其他语言低非常多。回归本质,学习编程语言还是为了低成本高效的解决实际的业务问题。
个人喜欢的编程语言风格
可以近实时更新变更
最好不需要长时间编译才能执行、应用启动快。
Java、 C 编译很慢,不适合频繁修改的项目。但是 PHP 、Node.js 修改即可见,可以极大提高开发效率。最好还能 hot-reload 就像很多前端工具一样,只要源码有一点变更,不需要刷新页面自动反应在浏览器中。Play framework 类似的自动加载功能也可以。
更进一步,能够在生产环境热加载就更好了,更新代码不影响用户。针对这一点,很多人乐了, PHP 默认就是这样的啊,部署后刷新 APC 缓存就可以实现。
这正是无状态、短链接的 HTTP 应用的优势,虽然随之而来的是性能相对降低更多 TCP 的开销,但是把问题变得简单很多。但是很多其他语言做到这点就很难了,比如大部分 Java 应用。
Elixir、Erlang 可以做到真正的任何情况下开着跑车换轮子。
关于热加载,见另一篇文章:编程开发常用的热加载工具。
支持并发执行
人们更习惯顺序执行的思路,并且大部分业务逻辑都是顺序执行的。但是为了降低延迟、提高性能,最好能在语言层面支持并发执行。比如,一个操作开始还未返回结果,就可以开始另一个操作。
这样调用远程 API 或者远程 RPC,耗时为最慢那一个操作的耗时。从这一点看,大部分流行语言都可以做到并发调用,但 PHP 难以做到。
轻量级执行进程或者线程
由于某些限制,某些业务逻辑不可避免的会因为大量计算、网络磁盘 IO 等占用一个执行进程或者线程。所以希望这个执行体能够尽量轻量级,很少的内存占用,很快的启动时间,很少的切换消耗,最好能在 IO 执行的时候自动让出计算资源。
并发和并行
我们更多关注并发,但是比较少关注并行。因为通过增加机器数量能抗住大量用户的请求比节省机器更加简单和迫切。
这也是很多互联网公司动辄几百台上千台服务器的现状。用户和请求量的多少由于业务逻辑的不同很难比较,只能比较机器数量了。
并发之进程模型
PHP 既是典型的这种模式。曾经见过某异步 PHP 框架 CS 高居不下,甚至比业务逻辑的 CPU 使用更高。
并发之线程模型
这种模型相对于进程模型好了很多,因为线程比进程轻量很多,创建、切换也快很多。
问题:线程和内核线程的关系为多对多,内核线程有限。能够调度的用户线程有限,无法充分利用多核性能。创建新线程消耗非常大。IO 阻塞无法释放计算资源。
每个 CPU 核心只能同时运行一个线程,多个线程之间需要切换调度(CS)。如果是 CPU 密集类型的计算,没有或者很少 IO 操作,最好启动 CPU 核心数量的线程。
但是如果有 IO 操作,比如 磁盘或者网络,多余 CPU 核心数的线程有效,因为 IO 操作的时候可以切换到其他线程执行 CPU 操作。
并发之 Fork-join 轻量级进程模型:
Fork-join 创建自己的进程池来执行小粒度的任务。
相对于 Erlang 那种真正的抢占式调度的 VM 实现或者操作系统的抢占式调度,Fork-join 模型非常简单,也意味着相比之下效率相对低。
Fork-join 针对计算密集操作设计,意味着无法告诉 F/J 框架你因为 IO 等待而释放一会儿计算资源。所以,一般需要将异步 IO 操作放到另外的线程池,FJ 只处理纯计算。
基于 Scala 的 Akka 既是这种模型。所以,假如处理不当, Akka 的 Actor 很容易阻塞执行线程,如果执行线程池的线程被耗光,整个应用将会僵死在那里。而 Erlang 则没有这个问题。
并发之 Erlang 轻量级进程模型:
VM 调度线程,将计算划分为非常小的执行单元。可以支持非常多的进程。IO 阻塞可以自动释放资源。真正的抢占式调度。
类型系统
静态类型可以避免很多失误。动态类型经常会出现不可预期的结果,这有悖于 UNIX 风格的最少意外原则。
动态类型可以让开发更加快速。强静态类型系统会执行很快,比如 Java,但是也可以在有必要的时候使用反射,比如很多 RPC 框架的实现 (当然也有更进一步的字节码修改技术)。
每个语言的类型系统都有自己的特点。
丰富的内置结构或者容器类
最好能够区分 Interface、Struct 和 Implementation。能够以比较统一的模式轻松的定义自己需要的结构体。
GC 系统
除非 Erlang 无可媲美的轻量级线程级别的 GC 。否则你要么需要记住和理解复杂的 GC 调优参数、要么像 PHP 那样过一段时间将进程杀掉重来。
元编程和 DSL 扩展性
在语法级别的抽象和封装更能提高开发效率。Elixr 中如何实现 DSL。
执行速度和性能
这点和并发并行模式、以及多核利用率密切相关。
UNIX 风格
简单说就是模块化;每个模块完成相对单一的功能、复制任务由多个模块组合完成。项目设计就像搭积木。不同模块之前的输入输出可以拼接。
另外就是极简风格。
依赖和库管理系统
这点 Node.js npm 是最好的依赖管理系统了,这样导致了 Node.js 社群库数量的爆发。因为创建和发布一个库实在是太容易了,找到需要的库也非常简单。
极大提高了开发效率。
打包和发布系统
最好能打包成单一文件,容易分发和部署。比如 Java 应用打包成 Fat Jar 包到处执行,或者 Golang 那样编译成单一文件。
日志系统
真实的项目、日志非常重要。之前的文章已经提到日志的重要性。所以好的内置日志系统或者比较统一高效的日志模式非常重要。
最好支持屏幕打印、写文件等等功能。这可能不能算一个编程语言的特性了,要看这个语言是不是有很好的日志类库。
Java 的 SLF 就是一个比较好的日志系统类库。
工具链
项目构建、编译、测试工具比较完善。
比如 Java、Scala 项目的 maven、sbt 。Erlang 项目可以用 rebar ,但是 Elixir 的 mix 友好的很多倍。
另外一个好的 REPL 命令行工具非常重要,因为这可以方便的侵入应用进行调试,或者测试一条代码片段。
比如 PHP 的 php -a, sbt, Clojure 的 lein, Erlang 的 erl, Elixir 的 iex 等等。
脚本执行
这是脚本语言的一大优势。小任务可以立刻创建一个脚本执行,而不需要修改、编译部署现有运行的应用。
这点对于小任务非常重要。Erlang 和 Elixir 都支持这样运行,escript 或者 Elixir 脚本。比如,连接到集群,读取状态或者进行一次性的数据操作,然后断开。
测试系统
最好有一种比较标准的单元测试模型。比如 Java、Node.js、Scala、Elixir 等等。
说了这么多,回到 Elixir。首先 Elixir 执行和 Erlang 没有任何差别。Erlang 的优点 Elixir 完全具备。比如:真正的抢占式调度;充分利用多核心并行执行;Actor 模型;监控树;透明的分布式;
极其高的稳定性;代码的热更新部署;函数式编程;模式匹配;等等。并且很多 Erlang 下工具也是可以直接使用。比如 entop 。
另外 Elixir 比 Erlang 多出的好处在于更加友好的语法、工具链、社群。很多之前写 Ruby 的开始写 Elixir,因为他们的语法最接近。