使用 Common Lisp 开发现代化的 Web 应用(一)

HTTP 服务器

Posted by David Gu on January 1, 2016

引子

在《黑客与画家》一书中,Paul Graham 多次提到了 Common Lisp 和他的创业产品 Viaweb。在那个动态网站还未像现在大放异彩的年代里,Viaweb 的出现绝对是会让人异常印象深刻的。《拒绝平庸》1一文中,Paul 甚至将 Lisp 称为『秘密武器』。据我所知,当时 Paul 所使用的 Common Lisp 实现是现在因为无人『认领』而略显尴尬的 CLISP ;如今距离当时已经过去了十七、八年了,无论是商业还是开源的 Lisp 实现,其性能与强健性都有了十分大的提升。多谢包括 Paul 的《On Lisp》,Peter 的《Practical Common Lisp》等等技术书籍的宣传与推广,Common Lisp 程序员的数量一直比较稳定,而各种各样开源项目的数量在包管理器 Quicklisp 问世后更是呈持续增长的趋势。眼看『形势一片大好』,然而,对于如今现代化的 Web 开发,Common Lisp 是否依然还是『秘密武器』呢?

例如,异步Asynchronous)的概念在目前 HTTP 服务器实现方面扮演了十分重要的角色,Common Lisp 是否有其独自的异步服务器呢?又例如,为了动态生成 HTML ,往往需要一个例如 Django Templates 那样的模版引擎,在 Common Lisp 的工具链中有合格的相关实现吗?连接数据库(包括关系型数据库以及 NoSQL)有现成的驱动与客户端吗?有哪些成熟的、现代化的 Common Lisp Web 开发框架?怎样部署与监控网站……

这个系列文章旨在对上述这些问题做一个尽可能全面的介绍;希望我能给国内 Common Lisp 爱好者们,尤其是想用 Common Lisp 开发 Web 应用的人们,带来一些实用的资料与启发。


Quicklisp

quicklisp_logo

在一切开始之前,对 Quicklisp 的介绍是很有必要的。在没有 Quicklisp 之前,尽管 Common Lisp 拥有 ASDF 这样的兼容十一种2(甚至更多?)实现的构建工具,然而整个社区却一直缺乏一个可用的包管理器。Quicklisp 的到来,意味着 Common Lisp 程序员可以像 Python 或者 Ruby 用户那样,只需输入 pip install ... 或者 gem install ... 即可完成对软件的安装。

获取与使用 Quicklisp 是极其简单的,你可以使用 curl 或者 wget 下载路径为 https://beta.quicklisp.org/quicklisp.lisp 的一个脚本,然后再用你的 Lisp 实现去加载即可。安装的大体流程如下:

$ curl -O https://beta.quicklisp.org/quicklisp.lisp
  % Total    % Received % Xferd  Average Speed   Time    Time     Time  Current
                                 Dload  Upload   Total   Spent    Left  Speed
100 49843  100 49843    0     0  33639      0  0:00:01  0:00:01 --:--:-- 50397

$ sbcl --load quicklisp.lisp
This is SBCL 1.3.1, an implementation of ANSI Common Lisp.
More information about SBCL is available at <http://www.sbcl.org/>.

SBCL is free software, provided as is, with absolutely no warranty.
It is mostly in the public domain; some portions are provided under
BSD-style licenses.  See the CREDITS and COPYING files in the
distribution for more information.

  ==== quicklisp quickstart 2015-01-28 loaded ====

    To continue with installation, evaluate: (quicklisp-quickstart:install)

    For installation options, evaluate: (quicklisp-quickstart:help)

$ (quicklisp-quickstart:install)
  ;;; then it will install Quicklisp into current user's home directory
  ;;; after the installation is finished, you can alse tell Quicklisp to
  ;;; add a new line to your lisp's initial file, which is something like this:
  ;; The following lines added by ql:add-to-init-file:
  ;; #-quicklisp
  ;; (let ((quicklisp-init (merge-pathnames "quicklisp/setup.lisp" (user-homedir-pathname))))
  ;;   (when (probe-file quicklisp-init)
  ;;    (load quicklisp-init)))

安装完成之后,你可以试着加载一个名叫 Alexandria 的实用工具库以及它的测试代码并测试之:

$ sbcl --load ~/quicklisp/setup.lisp
This is SBCL 1.3.1, an implementation of ANSI Common Lisp.
More information about SBCL is available at <http://www.sbcl.org/>.

SBCL is free software, provided as is, with absolutely no warranty.
It is mostly in the public domain; some portions are provided under
BSD-style licenses.  See the CREDITS and COPYING files in the
distribution for more information.
* (ql:quickload '(alexandria alexandria-tests))
To load "alexandria":
  Install 1 Quicklisp release:
    alexandria
; Loading "alexandria"
[package alexandria.0.dev]........................
..
To load "alexandria-tests":
  Load 1 ASDF system:
    alexandria-tests
; Loading "alexandria-tests"
[package alexandria-tests].
(ALEXANDRIA ALEXANDRIA-TESTS)
* (asdf:test-system :alexandria)
  ;;; a lot of output ...

用户在初始化自己的项目之后,可以将其复制到位于 ~/quicklisp/local-project/ 的目录下,通过手动调用函数 (ql:register-local-projects)(但这一步往往可以省略),Quicklisp 将会自动识别所有当前目录下的 .asd 文件并加入到 ASDF 的识别路径里去;只有这样,ASDF 才会知道去哪里寻找用户本地项目的 .asd 文件并解析之。

最后要说的是,Quicklisp 全面支持各个操作系统平台以及各个平台下的十一种 Common Lisp 实现;其本身的 API 特性十分丰富,来自日本的 Common Lisp 程序员 Fukamachi 利用这些 API 实现了一个非常实用的网站 Quickdocs。这个网站解析 Quicklisp 仓库里所有软件包的依赖关系、作者信息、项目源代码、 API 文档等等信息并发布在 Web 上。Fukamachi 为 Common Lisp 的开源社区贡献了很多的代码,尤其是在 Web 方面的贡献更是让人印象深刻,在包括本篇及之后的系列文章里,我都会陆续的提到。


HTTP 服务器

CL-HTTP3

CL-HTTP 可谓是一个十分古老的项目了,它的最初发布日期是 1994 年,距今已经有 20 多年的历史了。相对于接下来我要介绍的各个服务器实现,CL-HTTP 可谓是『大而全』,不仅实现了 Server,同时也有 Client 以及 Proxy 的相关实现。1996 年,CL-HTTP 成为了第一个完整支持 HTTP 1.1 协议的 Web 服务器。

Wikipedia 上列出了一个比较完整的特性功能清单:

  • 利用 Common Lisp 对象系统(CLOS)而构建的面向对象架构
  • 支持 SSL
  • 服务器
    • 处理静态文件
    • 动态的内容与表单
    • 利用 Lisp 宏来生成 HTML
    • 虚拟服务器
    • 网页的获取权限
    • 可自定义的日志
    • 服务器端包含(Server Side Includes,一种可以指挥服务器动态声称网页内容的 HTML 指令)4
  • 缓存代理
  • HTTP 客户端

有趣的是,该项目还曾被 比尔克林顿 的那一届的政府班子使用过,似乎是用来运营一个发布官方信息与新闻的网站。

由于年代确实太为久远了,想要用它来做产品级开发的话我个人认为是不太合适的,除非你拥有大量的时间去琢磨 CL-HTTP 那浩如烟海的源码与文档,而且你最好还要有购买一个专业版的 Lispworks 的经济实力,否则性能与稳定性上肯定是不合格的。

想要用 SBCL 或者 CCL 试运行 CL-HTTP 的朋友们可以参考这两篇博客:在 SBCL 等开源 Lisp 平台上运行 CL-HTTP (part 1)在 SBCL 等开源 Lisp 平台上运行 CL-HTTP (part 2)。博客中提到的的源代码可以从这里下载。除此以外,一个名为 A CL-HTTP Primer 的网站较为详细地介绍了 CL-HTTP 的 API 函数与宏。最后,想要阅读更多关于 CL-HTTP 资料的朋友们还可以参考这篇论文:A Common LISP Hypermedia Server

Allegro Serve5

Allegro Serve 是由商业 Lisp 实现 Allegro Common Lisp 的公司 Franz Inc. 所开发与维护的一个 HTTP 服务器,其历史同样也很悠久,似乎从发布至今已经过去了近 15 年了。虽然现在动态地生成 HTML 代码早已不是什么新鲜事,但在 Allegro Serve 刚发布的年代(约本世纪初)里,配合以 Lisp 宏为基础的 HTML 生成工具,一个完整的 HTTP 服务器实现仍然是充满竞争潜力的。以下为 Allegro Serve 官方主页上对其功能与特性的介绍:

  • 兼容 HTTP/1.1 并同时支持静态与动态页面的 Web 服务器
  • 一个解析与生成 HTML 的 DSL
  • HTTP 客户端
  • 服务器与客户端的 Secure Socket Layer (SSL)
  • 基于本地缓存的 Web 代理
  • 对包括客户端、服务器、代理和 SSL 完整的回归测试
  • 高性能
  • 合约条款保证其永远开源并鼓励将其应用在商业场景中

并且,我们近期加入了以下新的功能:

  • 全新的 publish 函数,可以从静态抑或动态数据中构建页面并缓存结果
  • 发布文件夹内容时的访问控制
  • 运行外部 CGI 程序的能力
  • 一个改进的虚拟主机系统,对于每个虚拟主机,支持不同的日志流和错误信息流

想要试用 Allegro Serve 的朋友们可以去下载一个 Allegro Common LispExpress 免费版本,并照着《Practical Common Lisp》第26章的内容进行一些简单的尝试。之前我们提到过,Allegro Serve 其实也是开源的,这个项目被 Franz Inc. 托管在了 Github 上。不过,Allegro Serve 只能在 Allegro Common Lisp 上运行。如果想要在其他开源实现上尝试的话,你就要使用 portableaserve 这个库了。

Hunchentoot6

Hunchentoot 是由 Edi Weitz 博士所编写的软件。这位来自德国的博士拥有很多高质量的 Common Lisp 项目,例如在某些基准测试中速度可以超过 Perl 的正则表达式解析库 CL-PPCREHunchentoot 的第一个版本 0.1.0 发布在 2005-12-31 这一天,而第一个正式版 1.0.0 则发布在了 2009-02-19。目前,它的最新版本号为 1.2.34,更新时间为 2015-07-06

你可以从 Quicklisp 上直接加载 Hunchentoot,并测试生成一个最简单的页面:

CL-USER> (ql:quickload :hunchentoot)
To load "hunchentoot":
  Load 2 ASDF systems:
    cl-ppcre uiop
  Install 17 Quicklisp releases:
    alexandria babel bordeaux-threads cffi chunga cl+ssl
    cl-base64 cl-fad flexi-streams hunchentoot md5 rfc2388
    trivial-backtrace trivial-features trivial-garbage
    trivial-gray-streams usocket
	;;; blah blah blah ....
CL-USER> (hunchentoot:define-easy-handler (say-name :uri "/say") (name)
	   (setf (hunchentoot:content-type*) "text/plain")
	   (format nil "The name is ~A.~%" name))
SAY-NAME
CL-USER> (hunchentoot:start (make-instance 'hunchentoot:easy-acceptor :port 8080))
#<HUNCHENTOOT:EASY-ACCEPTOR (host *, port 8080)>

打开浏览器,输入 localhost:8080 可以看到:

hunchentoot

hunchentoot

在实现方面,Hunchentoot 采用的是传统的多线程机制,也就是说,对于每一个请求,Hunchentoot 都会使用一个额外的线程来处理。然而,Hunchentoot 对于总体的线程数量是有限制的;于是,当某一时刻并发的请求数远远大于那个最大线程数时,其性能将有可能会大幅度地下滑。这样的模型可以被称为 Multi-process I/O 模型。所以我们说,Hunchentoot 虽然在稳定性和强健性方面都非常优秀,但当面对高并发的场景时,其局限性是很明显的。

值得一提的是,Edi Weitz 博士本人也曾在邮件列表中说过,Hunchentoot 本身是在 Lispworks 平台上开发与测试的。所以,也许在 Lispworks 平台上,Hunchentoot 的表现会更出色吧!但不管怎样,由于其本身架构的局限性,Hunchentoot 并不适合高并发场景;但是换个角度看的话,如果一个网站的更多开销并不是花在网络 I/O 上,那么像 Hunchentoot 这样依赖于多线程机制的服务器反而就是合适的选择了。接下来,我将会介绍两个异步服务器实现。


Wookie7

wookie

Wookie 是由 andrew lyon 编写的一个基于异步机制的服务器实现,Wookie 一名则取自作者的宠物犬。虽然它目前的状态是『公测』(Beta),但事实上它已被部署在生产环境很久了,有兴趣的朋友们可以参考这个名叫 Turtl应用 以及这个应用的 API 接口

也许,比 Wookie 更有趣的是它所依赖的 cl-async 库。cl-async 依赖于 libuv,为 Common Lisp 提供了相对来讲很完整的异步 I/O 机制。所以,在使用 Quicklisp 加载 Wookie 之前,请先确保你已经安装了 libuv;使用 Wookie 的方法在其文档页面上写的非常详细了,在此不再赘述,请 (ql:quickload :wookie) 后一步一步按照这个页面上的步骤进行尝试与学习即可。


Woo8

Woo 的作者是上文我们提到过的 Fukamachi。这个实现是目前性能最好的用 Lisp 实现的服务器了,请先看下面的性能对比9

对于每个请求,返回字符串 “Hello, World” :

benchmarks-1

当使用多个 CPU Core 时:

benchmarks-2

其性能的强悍确实让人眼前一亮。与 Wookie 不同的是,Woo 依赖于另一个高性能的 I/O 库 libev。也许有人会说,『好吧,Lisp 性能这么「反常」,一定是这个 C 写的 libev 的功劳。』但事实上,libev 只是一个在类 Unix 系统上 epoll, kqueue, POSIX select, poll 十分轻量级的封装10Woo 的高性能更多依赖于其实现架构(Evented I/O)的选择,以及,众多高性能的 Common Lisp 软件包:fast-http, quri, fast-io,等等。fast-http 以及 quri 的作者同样是 Fukamachi。前者提供了解析 HTTP 请求与响应的功能,在这个测试中,fast-http 比 C 写的 http-parse 快了 1.25 倍quri 则是一个为了取代 puri 的 URI 解析库,在这个测试中,quripuri 直接快了 6.6 倍

值得一提的是,在测试中,Woo 的性能要比 Node.js 高出不少;我想,这会使很多人更新他们对 Lisp 性能低下的刻板印象吧。

另外,在这个 issue 中,开发者们提到了可以使用一种名为 [SprayList] 的数据结构来提升性能。这是笔者目前比较关注的。如果这个方案是有效的,我相信 Woo 的性能又将会提升不少。


尾声

至此,关于 Common Lisp 实现的 HTTP 服务器的介绍就将告一段落了。我一共介绍了包括 CL-HTTP, Alegro Serve, Hunchentoot, Wookie, Woo 在内的五种实现。我个人认为这些实现的质量都很高。也许有人会发现,除了 Hunchentoot 以外,我并没有详细介绍各个服务器实现的使用方法。究其缘由,除了他们的文档都很详尽以外,现在 Common Lisp 程序员已经不会再直接依赖于一个特定的服务器 API 来编写 Web App 了。一旦代码是依赖于,例如,Hunchentoot,那么当开发人员想将服务器更换为 Woo 的时候,就可能必须要修改大量的代码(现在一般情况下 Hunchentoot 会被选择作为开发调试时的服务器,而在生产环境则会选择 Woo)。FukamachiClack 解决了这个问题,不过这是下一篇博客的内容了。


引用

  1. Beating the Average 

  2. ASDF, Supported Implementations are shown here

  3. CL-HTTP, Wikipedia 

  4. Server Side Includes, Wikipedia 

  5. Allegro Serve 

  6. Hunchentoot, the Homepage and Documentation 

  7. Wookie, Documentation Page 

  8. Woo, Github Page 

  9. Performance benchmarks between Hunchentoot, Wookie, Tornado, Node.js, Unicorn+Nginx and Go 

  10. Woo: Writing a fast web server, SlideShare, Slide No. 28