部署你的web应用
我不知道别人怎样,但我自己是喜欢编程胜过做系统管理。但事实是,最终,你需要将你 的应用放到服务器上,而且很有可能你会是那个搭建它的人。
在Haskell web社区有一些前景不错的项目在努力让部署变得更容易。未来,我们甚至可 能有这样的服务,它可以让你只用一条命令就完成应用部署。
但我们还没到那一步。即使我们到了,这样的解决方案也不会对每个人都适用。本章讲述 几种不同的部署方案,并对不同情况下应该选择什么方案提供一些一般性建议。
注意 | 虽然本章的内容不依赖于Yesod的特定版本,但因为最新的一些发展,如FP Complete的应用服务器以及Keter等,我不认为本章的所有建议都是准确的。本章大部分 内容没有改动,但我可能会在接下来几个月再次更新它。(2013年6月23日) |
编译
首先:你怎么构建(build)你的生产环境应用呢?如果你使用的是脚手架站点,只要运行 cabal build即可。同时我建议构建前先做一些清理工作,以保证没有任何缓存信息 。因此一个简单的构建组合命令是:
- 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文件,比如:
- daemon off; # 让nginx在前台运行,方便监控状态
- events {
- worker_connections 4096;
- }
- http {
- server {
- listen 80; # Nginx侦听的端口
- server_name www.myserver.com;
- location / {
- proxy_pass http://127.0.0.1:4321; # 反向代理至你的Yesod应用
- }
- }
- }
你想添加多少个server块都可以。一个常见的修改是确保用户总是以www域名前缀访问你 的网站,从而保证了使用经典URL的RESTful原则。(反过来要总是去掉www前缀也很容易, 只要确保nginx配置文件及站点的approot都配置正确即可。)在这种情况下,我们可以增 加下面的块:
- server {
- listen 80;
- server_name myserver.com;
- rewrite ^/(.*) http://www.myserver.com/$1 permanent;
- }
强烈建议的一项优化是从单独的域名托管静态文件,从而绕过cookie的传输开销。假设我们 的静态文件都存放在站点目录的static文件夹内,而站点目录位于 /home/michael/sites/mysite,配置文件需要写成这样:
- server {
- listen 80;
- server_name static.myserver.com;
- root /home/michael/sites/mysite/static;
- # 因为yesod-static会将文件内容的哈希值追加为静态文件的请求参数,
- # 我们可以将过期时间设置为很久以后,而不用担心用户会看到过时的内容。
- expires max;
- }
为了让它能工作,你的站点必须正确的将静态URL重写到这个域名。脚手架站点通过 Settings.staticRoot和urlRenderOverride函数让这件事很容易。然而,如果你 只想要nginx提供更快的静态文件托管,而不使用单独的域名,你可以将配置文件写成这 样:
- server {
- listen 80; # Nginx侦听端口
- server_name www.myserver.com;
- location / {
- proxy_pass http://127.0.0.1:4321; # 反向代理至你的Yesod应用
- }
- location /static {
- root /home/michael/sites/mysite; # 注意这里**不用**写/static
- expires max;
- }
- }
服务器进程
很多人对Apache/mod_php或Lighttpd/FastCGI那样的配置很熟悉,这些服务器会自动产生 web应用的进程。对于nginx,不管是用反向代理还是FastCGI,都不是这样:你需要自己 负责去运行进程。我强烈建议使用一个监控程序,它能够在你的程序崩溃时自动帮你重启 。有很多好工具可选,比如angel或daemontools。
作为一个具体的例子,下面是一个Upstart配置文件。该文件必须存成 /etc/init/mysite.conf:
- description "My awesome Yesod application"
- start on runlevel [2345];
- stop on runlevel [!2345];
- respawn
- chdir /home/michael/sites/mysite
- 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)会比绑定端口号更方便,特别是在一台服务器托管多 个应用的情况下。一个加载你的应用的脚本可以这样写:
- spawn-fcgi \
- -d /home/michael/sites/mysite \
- -s /tmp/mysite.socket \
- -n \
- -M 511 \
- -u michael \
- -- /home/michael/sites/mysite/dist/build/mysite-fastcgi/mysite-fastcgi
你还需要将你的前端服务器配置成能通过FastCGI与你的应用通信。在Nginx中这很容易:
- server {
- listen 80;
- server_name www.myserver.com;
- location / {
- fastcgi_pass unix:/tmp/mysite.socket;
- }
- }
这些看起来应该都很熟悉。最后一个技巧是,在Nginx中,你需要手动指定所有的FastCGI 变量。建议将它们保存在单独的文件中(比如fastcgi.conf),然后用include fastcgi.conf;加到http块的末尾。要与WAI配合工作,文件内容应该是:
- fastcgi_param QUERY_STRING $query_string;
- fastcgi_param REQUEST_METHOD $request_method;
- fastcgi_param CONTENT_TYPE $content_type;
- fastcgi_param CONTENT_LENGTH $content_length;
- fastcgi_param PATH_INFO $fastcgi_script_name;
- fastcgi_param SERVER_PROTOCOL $server_protocol;
- fastcgi_param GATEWAY_INTERFACE CGI/1.1;
- fastcgi_param SERVER_SOFTWARE nginx/$nginx_version;
- fastcgi_param REMOTE_ADDR $remote_addr;
- fastcgi_param SERVER_ADDR $server_addr;
- fastcgi_param SERVER_PORT $server_port;
- 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。
- Options +ExecCGI
- AddHandler cgi-script .cgi
- Options +FollowSymlinks
- RewriteEngine On
- RewriteRule ^/f5/snoyman/public/blog$ /blog/ [R=301,S=1]
- RewriteCond $1 !^bloggy.cgi
- RewriteCond $1 !^static/
- RewriteRule ^(.*) bloggy.cgi/$1 [L]
第一条RewriteRule是为了处理子文件夹。特别是,它将/blog的请求重定向到 /blog/。第一条RewriteCond防止直接请求可执行文件,第二条允许Apache托管静态 文件,最后一行是实际上的重写(请求)。
lightppd上的FastCGI
在这个例子中,我没有涉及一些基本的FastCGI设置,比如mime类型。在生产环境我还有 一个更复杂的配置文件,会在请求路径缺少“www.”前缀时自动加上,并从单独的域名托管 静态文件。然而,这个例子可以说明基本的情况。
这里,“/home/michael/fastcgi”是fastcgi应用。目标是将所有请求重写为以“/app”开头 ,然后所有以“/app”开头的请求,都会经过FastCGI可执行文件。
- server.port = 3000
- server.document-root = "/home/michael"
- server.modules = ("mod_fastcgi", "mod_rewrite")
- url.rewrite-once = (
- "(.*)" => "/app/$1"
- )
- fastcgi.server = (
- "/app" => ((
- "socket" => "/tmp/test.fastcgi.socket",
- "check-local" => "disable",
- "bin-path" => "/home/michael/fastcgi", # full path to executable
- "min-procs" => 1,
- "max-procs" => 30,
- "idle-timeout" => 30
- ))
- )
lighttpd上的CGI
这与FastCGI的版本基本一样,但告诉lighttpd要运行以“.cgi”结尾的CGI可执行文件。这 个例子中,可执行文件位于“/home/michael/myapp.cgi”。
- server.port = 3000
- server.document-root = "/home/michael"
- server.modules = ("mod_cgi", "mod_rewrite")
- url.rewrite-once = (
- "(.*)" => "/myapp.cgi/$1"
- )
- cgi.assign = (".cgi" => "")