部署你的web应用

我不知道别人怎样,但我自己是喜欢编程胜过做系统管理。但事实是,最终,你需要将你 的应用放到服务器上,而且很有可能你会是那个搭建它的人。

在Haskell web社区有一些前景不错的项目在努力让部署变得更容易。未来,我们甚至可 能有这样的服务,它可以让你只用一条命令就完成应用部署。

但我们还没到那一步。即使我们到了,这样的解决方案也不会对每个人都适用。本章讲述 几种不同的部署方案,并对不同情况下应该选择什么方案提供一些一般性建议。

注意 虽然本章的内容不依赖于Yesod的特定版本,但因为最新的一些发展,如FP Complete的应用服务器以及Keter等,我不认为本章的所有建议都是准确的。本章大部分 内容没有改动,但我可能会在接下来几个月再次更新它。(2013年6月23日)

编译

首先:你怎么构建(build)你的生产环境应用呢?如果你使用的是脚手架站点,只要运行 cabal build即可。同时我建议构建前先做一些清理工作,以保证没有任何缓存信息 。因此一个简单的构建组合命令是:

  1. cabal clean && cabal configure && cabal build

需要部署的文件

如果是一个脚手架应用,基本上有三组文件需要部署:

  • 你的可执行文件。

  • 配置文件夹。

  • 静态文件夹。

其它文件都会被编译进可执行文件,比如莎氏模板文件。

然而有一点是需要注意的:就是config/client_session_key.aes文件。这个文件包 含了服务器端用来加密客户端会话cookie的密钥。如果没有这个文件,Yesod会自动生成 一个。在实践中这意味着,如果你在部署时没有包含这个文件,重新部署完以后所有用户 都需要重新登录。如果你遵循上面的建议,在部署时包含整个config文件夹,这个问 题能得到部分解决。

另一半的解决方案是要保证你一旦生成了config/client_session_key.aes文件,以 后在部署都使用同一个。要保证这点最简单的办法是将其加入你的版本控制中。然而,如 果你的软件库是开源的,这样会很危险:任何能访问你软件库的人都能够伪造登录身份!

这里描述的问题本质上是系统管理范畴的,而不是程序开发范畴的。Yesod没有内置的方 法用来安全存放客户端会话密钥。如果你有一个开源软件库,且无法信任所有能够访问你 软件库源代码的人,那就应该想出一个安全存储客户端会话密钥的方案。

Warp服务器

正如我们之前提过的,Yesod构建于Web应用接口(WAI: Web Application Interface)之上 ,因此能够运行在任何WAI后端上。本章成文之时,有以下后端可选:

  • Warp

  • FastCGI

  • SCGI

  • CGI

  • Webkit

  • Yesod开发服务器

最后两种不适合在生产环境部署。其它四种理论上都可以在生产环境使用。在实践中, CGI后端很可能会非常低效,因为每个连接都需要产生(spawn)一个新的进程。SCGI没有得 到像Warp(通过反向代理)和FastCGI那么好的支持。

因此,在剩下的两种选项里,强烈推荐Warp,理由是:

  • 速度更快。

  • 与FastCGI一样,它可以使用反向HTTP代理运行在如Nginx一类的前端服务器(frontend server)之后。

  • 此外,它本身就是一个功能完备的服务器,因此可以不用前端服务器单独使用。

所以剩下最后一个问题是:Warp应该单独使用,还是通过反向代理在前端服务器后使用? 对于大多数应用场景,我推荐第二种,因为:

  • Warp虽然很快,但它是作为一个应用服务器优化的,而不是作为静态文件服务器。

  • 使用Nginx,你可以配置虚拟主机来从单独的域名托管静态内容。(Warp也可以做到这一  点,但配置起来会更复杂)。

  • 你可以将Nginx用作负载均衡或SSL代理。(虽然warp-tls也可以只用Warp运行一个https  站点。)

所以我的最终建议是:给Nginx配置一个到Warp的反向代理。

注意 Yesod社区里很多人在这点上与我有分歧。他们认为单独使用Warp,跳过Nginx那一 步是更好的选择,因为性能得到提升且复杂性下降。你可以任意使用其中一种,它们都是 完全合理的。

配置

通常,Nginx会侦听80端口,Yesod/Warp会侦听非特权端口(假设是4321)。你需要提供一 个nginx.conf文件,比如:

  1. daemon off; # 让nginx在前台运行,方便监控状态
  2. events {
  3. worker_connections 4096;
  4. }
  5.  
  6. http {
  7. server {
  8. listen 80; # Nginx侦听的端口
  9. server_name www.myserver.com;
  10. location / {
  11. proxy_pass http://127.0.0.1:4321; # 反向代理至你的Yesod应用
  12. }
  13. }
  14. }

你想添加多少个server块都可以。一个常见的修改是确保用户总是以www域名前缀访问你 的网站,从而保证了使用经典URL的RESTful原则。(反过来要总是去掉www前缀也很容易, 只要确保nginx配置文件及站点的approot都配置正确即可。)在这种情况下,我们可以增 加下面的块:

  1. server {
  2. listen 80;
  3. server_name myserver.com;
  4. rewrite ^/(.*) http://www.myserver.com/$1 permanent;
  5. }

强烈建议的一项优化是从单独的域名托管静态文件,从而绕过cookie的传输开销。假设我们 的静态文件都存放在站点目录的static文件夹内,而站点目录位于 /home/michael/sites/mysite,配置文件需要写成这样:

  1. server {
  2. listen 80;
  3. server_name static.myserver.com;
  4. root /home/michael/sites/mysite/static;
  5. # 因为yesod-static会将文件内容的哈希值追加为静态文件的请求参数,
  6. # 我们可以将过期时间设置为很久以后,而不用担心用户会看到过时的内容。
  7. expires max;
  8. }

为了让它能工作,你的站点必须正确的将静态URL重写到这个域名。脚手架站点通过 Settings.staticRoot和urlRenderOverride函数让这件事很容易。然而,如果你 只想要nginx提供更快的静态文件托管,而不使用单独的域名,你可以将配置文件写成这 样:

  1. server {
  2. listen 80; # Nginx侦听端口
  3. server_name www.myserver.com;
  4. location / {
  5. proxy_pass http://127.0.0.1:4321; # 反向代理至你的Yesod应用
  6. }
  7. location /static {
  8. root /home/michael/sites/mysite; # 注意这里**不用**写/static
  9. expires max;
  10. }
  11. }

服务器进程

很多人对Apache/mod_php或Lighttpd/FastCGI那样的配置很熟悉,这些服务器会自动产生 web应用的进程。对于nginx,不管是用反向代理还是FastCGI,都不是这样:你需要自己 负责去运行进程。我强烈建议使用一个监控程序,它能够在你的程序崩溃时自动帮你重启 。有很多好工具可选,比如angel或daemontools。

作为一个具体的例子,下面是一个Upstart配置文件。该文件必须存成 /etc/init/mysite.conf:

  1. description "My awesome Yesod application"
  2. start on runlevel [2345];
  3. stop on runlevel [!2345];
  4. respawn
  5. chdir /home/michael/sites/mysite
  6. exec /home/michael/sites/mysite/dist/build/mysite/mysite

一旦有这个文件,启动你的应用只需要用sudo start mysite命令。

FastCGI

有些人可能更喜欢在部署时用FastCGI。这种情况下,你会需要用到额外的工具。FastCGI 的工作方式是通过文件描述符(file descriptor)来接收新的连接。C语言库假设这个文件 描述符是0(标准输入),因此你需要用spawn-fcgi程序将你应用的标准输入绑定到正确的 套接字(socket)上。

用Unix的命名套接字(named socket)会比绑定端口号更方便,特别是在一台服务器托管多 个应用的情况下。一个加载你的应用的脚本可以这样写:

  1. spawn-fcgi \
  2. -d /home/michael/sites/mysite \
  3. -s /tmp/mysite.socket \
  4. -n \
  5. -M 511 \
  6. -u michael \
  7. -- /home/michael/sites/mysite/dist/build/mysite-fastcgi/mysite-fastcgi

你还需要将你的前端服务器配置成能通过FastCGI与你的应用通信。在Nginx中这很容易:

  1. server {
  2. listen 80;
  3. server_name www.myserver.com;
  4. location / {
  5. fastcgi_pass unix:/tmp/mysite.socket;
  6. }
  7. }

这些看起来应该都很熟悉。最后一个技巧是,在Nginx中,你需要手动指定所有的FastCGI 变量。建议将它们保存在单独的文件中(比如fastcgi.conf),然后用include fastcgi.conf;加到http块的末尾。要与WAI配合工作,文件内容应该是:

  1. fastcgi_param QUERY_STRING $query_string;
  2. fastcgi_param REQUEST_METHOD $request_method;
  3. fastcgi_param CONTENT_TYPE $content_type;
  4. fastcgi_param CONTENT_LENGTH $content_length;
  5. fastcgi_param PATH_INFO $fastcgi_script_name;
  6. fastcgi_param SERVER_PROTOCOL $server_protocol;
  7. fastcgi_param GATEWAY_INTERFACE CGI/1.1;
  8. fastcgi_param SERVER_SOFTWARE nginx/$nginx_version;
  9. fastcgi_param REMOTE_ADDR $remote_addr;
  10. fastcgi_param SERVER_ADDR $server_addr;
  11. fastcgi_param SERVER_PORT $server_port;
  12. fastcgi_param SERVER_NAME $server_name;

桌面

另一个很棒的(nifty)后端是wai-handler-webkit。这个后端将Warp与QtWebkit结合来创 建一个用户可以双击运行的程序。是给你的应用提供离线版本的好方法。

Yesod很好的一点是你所有的模板都会编译进可执行文件,因此不需要与程序一起分发。 不过,静态文件还是要分发。

注意 实际上也支持将静态文件直接嵌入可执行文件,详情查阅yesod-static文档。

一种类似的方法是用wai-handler-launch,而不用QtWebkit库。它会启动Warp服务器然后 打开用户的默认浏览器。这里还有个小花招:为了知道用户还在使用网站, wai-handler-launch给每个HTML页面插入一段用Javascript写的“ping”代码。如果 wai-handler-launch在两分钟内没收到ping,它就会关闭。

在Apache上运行CGI

CGI和FastCGI在Apache上几乎是一样的,因此配置文件可以直接拿来用。你基本上只要完 成两件事:

  • 让服务器用(Fast)CGI托管你的文件。

  • 将你网站的所有请求重写至(Fast)CGI可执行文件。

下面是一个托管博客程序的配置文件,可执行文件名为“bloggy.cgi”,位于document根目 录的“blog”子文件夹里。该应用位于/f5/snoyman/public/blog。

  1. Options +ExecCGI
  2. AddHandler cgi-script .cgi
  3. Options +FollowSymlinks
  4.  
  5. RewriteEngine On
  6. RewriteRule ^/f5/snoyman/public/blog$ /blog/ [R=301,S=1]
  7. RewriteCond $1 !^bloggy.cgi
  8. RewriteCond $1 !^static/
  9. RewriteRule ^(.*) bloggy.cgi/$1 [L]

第一条RewriteRule是为了处理子文件夹。特别是,它将/blog的请求重定向到 /blog/。第一条RewriteCond防止直接请求可执行文件,第二条允许Apache托管静态 文件,最后一行是实际上的重写(请求)。

lightppd上的FastCGI

在这个例子中,我没有涉及一些基本的FastCGI设置,比如mime类型。在生产环境我还有 一个更复杂的配置文件,会在请求路径缺少“www.”前缀时自动加上,并从单独的域名托管 静态文件。然而,这个例子可以说明基本的情况。

这里,“/home/michael/fastcgi”是fastcgi应用。目标是将所有请求重写为以“/app”开头 ,然后所有以“/app”开头的请求,都会经过FastCGI可执行文件。

  1. server.port = 3000
  2. server.document-root = "/home/michael"
  3. server.modules = ("mod_fastcgi", "mod_rewrite")
  4.  
  5. url.rewrite-once = (
  6. "(.*)" => "/app/$1"
  7. )
  8.  
  9. fastcgi.server = (
  10. "/app" => ((
  11. "socket" => "/tmp/test.fastcgi.socket",
  12. "check-local" => "disable",
  13. "bin-path" => "/home/michael/fastcgi", # full path to executable
  14. "min-procs" => 1,
  15. "max-procs" => 30,
  16. "idle-timeout" => 30
  17. ))
  18. )

lighttpd上的CGI

这与FastCGI的版本基本一样,但告诉lighttpd要运行以“.cgi”结尾的CGI可执行文件。这 个例子中,可执行文件位于“/home/michael/myapp.cgi”。

  1. server.port = 3000
  2. server.document-root = "/home/michael"
  3. server.modules = ("mod_cgi", "mod_rewrite")
  4.  
  5. url.rewrite-once = (
  6. "(.*)" => "/myapp.cgi/$1"
  7. )
  8.  
  9. cgi.assign = (".cgi" => "")