月度归档:2014年07月

通用网关协议 (CGI) 进化史 番外篇

WSGI

在 python 大红大紫的今天, 除了 FastCGI, 我们听到的最多的可能就要数 WSGI 了, 那么 WSGI 又是啥呢? 它和 CGI, SCGI, FastCGI 又有什么关系呢?

你应该能猜得到, 既然 WSGI 也归到了 “通用网关协议 (CGI) 进化史” 这一系列里面, 那么 WSGI 和 CGI 肯定也是有点关系的.

WSGI 是专为 python 设计的协议, 其包装了 FastCGI 协议, WSGI server 一般也都能够作为 SCGI server, 或者 FastCGI server 运行.

如果说 FastCGI 传递信息时用的是一种底层字节流的形式, 那么 WSGI 就是将这字节流结构化为 python 对象, 这使得在 python 中进行 web 开发时, 你只要写一个文件上传的表单, 通过 WSGI 你就能直接获得这个文件的对象, 而不必自己去读取 HTTP 请求体来接收上传的文件.

Python 养了很多的框架, 如 Zope, Quixote, Webware, SkunkWeb, PSO 以及 Twisted Web. 如此之多的框架对于 python 用户来说可能反倒是个问题, 因为一般情况下这些框架并不是适配于所有的 webserver, 于是选择某个框架也就意味着你相应的只能使用某几个 webserver, 反之亦然.

然而, 瞧瞧 java, 尽管 java 拥有很多的网络应用程序开发框架, 然而所有的框架以及所有的 java 容器都遵循同一个标准: servlet API. 遵循 servlet api 的框架如 struts, spring, hibernate, 而容器则有 tomcat, jboss 等.

如果在 python 中也存在这样的 api, 那么在 python 开发中, 框架的选择与 webserver 的选择就也可以互不干扰了.

因此, 一种简单通用的接口被定义出来, 旨在统一 webserver 与 web application (或者是框架) 直接的通信接口: the Python Web Server Gateway Interface (WSGI).

webserver 对 WSGI 的支持

Apache

apache 里有一个模块叫做 mod_wsgi, 是一个第三方模块, apache 通过这个模块提供对 wsgi 的支持. 这个模块既能够运行在 embeded 模式下, 也能够运行在 daemon 模式下. 这里我们讨论他运行在 embeded 模式下的情况.

在 embeded 模式下, mod_wsgi 的配置与 mod_cgi 的配置十分相似, 首先, 我们创建一个 wsgi 脚本, 叫做 myapp.wsgi:

def application(environ, start_response):
    status = '200 OK'
    output = 'Hello World!'

    response_headers = [('Content-type', 'text/plain'),
                    ('Content-Length', str(len(output)))]
    start_response(status, response_headers)

    return [output]

可以将其放在 /usr/local/apache/wsgi-bin 目录下 (当然你可以随便放, 但要保证相关的权限, 还记的 cgi 脚本放在 cgi-bin 目录下吗?), 这里定义的 callable 的名字是 “application”, 这是 WSGI 协议里规定的, 必须就叫这个名字.

然后, 还记得在使用 mod_cgi 模块时, 指定 cgi 脚本所使用的 ScriptAlias 指令吧? 现在要制定 WSGI 脚本, 我们需要使用 WSGIScriptAlias 指令, 在 apache 的配置文件添加如下这句:

WSGIScriptAlias /myapp /usr/local/apache/wsgi-bin/myapp.wsgi

打开浏览器, 访问 http://localhost/myapp, 就能看到你的 wsgi 脚本的运行结果了.

当然, WSGIScriptAlias 指令也是可以这么用的:

WSGIScriptAlias /myapp /usr/local/apache/wsgi-bin/

这样一来, wsgi-bin/ 整个目录下的文件都能够被当作 wsgi 脚本运行, 这点和 mod_cgi 模块的 ScriptAlias 指令是一致的.

关于 mod_wsgi 模块还有很多强大的功能, 详情请看参考2.

mod_wsgi embeded 与 mod_cgi 的区别

相比 FastCGI, SCGI, WSGI 与 CGI 应该是最像的, 不过当然也是有区别的.

一方面 WSGI 提供了更加丰富的指令, 第二, wsgi 程序并不是另起一个进程执行的, 而是和 apache 处在同一个进程里 (和 apche 的 worker process 处于同一进程). 这一点通过上面的 wsgi 脚本其实也能看出来, 因为上面的 wsgi 脚本里第一行没有 shebang, 这就意味着不需要调用一个外部程序去执行这个脚本.

mod_wsgi 的另一种模式

刚才说了 mod_wsgi 还可以运行在 daemon 模式下, 在 embeded 模式下, wsgi 脚本改变了就需要重启 apache 服务器, 而在这种模式下, daemon 进程能够监控 wsgi 脚本的改变, 一旦脚本改变 daemon 脚本就回重启, 而不需要重启 apache.

在这种模式下, mod_wsgi 与 fastcgi 的方式更像了一些, 但是 mod_wsgi 的 daemon 进程与 apache worker 进程是父子进程的关系 (我没有查资料, 我是靠 mod_wsgi daemon 模式下的一个配置指令: SetENV 猜测出来的), 而不是通过什么 socket 通信的, 这点与 fastcgi 不一样.

关于这种模式的详情可以参见 参考1, 参考2.

mod_wsgi 的新一代

mod_wsgi 是个很不错的项目, 这个项目持续了很久, 感谢它的维护者们, 这个项目从 Google Code 已经迁移到了 Github 上, 开启了新的篇章.

上面所介绍的关于 wsgi 的内容, 全部都是 mod_wsgi 的开发者们在 Google Code 上就已经做好的了事情, 当项目迁移到 Github 之后, 开发者们又开发了新一代的 mod_wsgi.

最新的一代 mod_wsgi 已经不必安装为 apache 的模块了, 它可以直接安装到你的 python 中, 并且能够作为独立的 http server 启动 (好像还是调用了 apache 的可执行程序, 我没有深究, 不重要了), 它有一个新的名字: mod_wsgi-express.

参考

  1. mod_wsgi 官方的快速配置文档, 强烈推荐: http://code.google.com/p/modwsgi/wiki/QuickConfigurationGuide
  2. mod_wsgi 官方的详细配置文档, 强烈推荐: http://code.google.com/p/modwsgi/wiki/ConfigurationGuidelines
  3. mod_wsgi 项目新生代, Github 主页: https://github.com/GrahamDumpleton/mod_wsgi
  4. PEP 333, WSGI 的标准文档: http://legacy.python.org/dev/peps/pep-0333/#specification-overview
  5. 这篇主题很好的介绍了 FastCGI 和 WSGI 的区别: http://stackoverflow.com/questions/1747266/is-there-a-speed-difference-between-wsgi-and-fcgi
  6. 这篇主题的作者经历很不错, 提问的很广泛, 回答者们的答案虽然都得票不多, 胆答得都很值得一看: http://stackoverflow.com/questions/2532477/mod-cgi-mod-fastcgi-mod-scgi-mod-wsgi-mod-python-flup-i-dont-know-how-m
  7. 这个主题, 回答者很精彩的介绍了 WSGI, CGI 等的关系: http://stackoverflow.com/questions/219110/how-python-web-frameworks-wsgi-and-cgi-fit-together

通用网关协议 (CGI) 进化史 (下篇)

FastCGI

FastCGI 和 SCGI 是比较类似的, 但 FastCGI 明显要比 SCGI 流行很多, 可以看一下参考1 中维基页面, 实现 FastCGI 的 webserver, 以及语言 binding 明显要比 SCGI 的维基页中介绍的多.

FastCGI 也有自己的官方网站, 其官方网站可以看出 FastCGI 的一系列优点 (这些优点对 SCGI 也适用):

  • FastCGI 够简单, 它实际只是 CGI 以及一些扩展.
  • 如 CGI 一样, FastCGI 也是和语言无关的.
  • 如 CGI 一样, FastCGI 进程与 webserver 的进程是隔离的, 这相比模块化的方案来说, 提高了安全性. (mod_perl, mod_php 这样的方案, 如果模块中有 bug, 会导致 webserver 受影响)
  • 如 CGI 一样, FastCGI 并不是与 webserver 的架构结合在一起的, 而模块化的方式, 是与 webserver 的架构结合在一起的.

当然, 除了兼具 CGI 的好处, FastCGI 还具备以下两点主要好处:

  • 分布式计算: FastCGI 进程不必和 webserver 运行在同一台机器上.
  • 多角色: CGI 脚本能够控制某一个 HTTP 请求的响应值, 而 FastCGI 能做的事情更多, 比如 HTTP 的验证机制.

按照惯例, 我们看一下在各个 webserver 里配置 FastCGI 的方式.

Apache

(这一节的内容可以参见参考2)

Apache 最早通过 mod_fcgid 实现的 FastCGI, 这是一个第三方的后来也被 ASF 组织承认的模块. 不过, 它只支持 unix socket 模式, 不支持 tcp socket.

另一个第三方的模块是 mod_fastcgi, 曾经不能很好的被编译为 apache 2.4.x 的模块, 后来被修复了.

apache 2.4.x 推出了另一个 mod_proxy_fcgi 模块, 这是最新的支持 FastCGI 的模块, 类似于 mod_proxy_scgi.

至于这三个模块的配置文件怎么写, 就不在这里多费口舌了, 自行 google 之.

Lighttpd

自己带着 mod_fastcgi 模块

Nginx

也有自己的 ngx_http_fastcgi_module 模块

FastCGI Server

这才是最重要的部分吧, 和 SCGI 一样, FastCGI server 的是实现也是和语言相关的. 因为我对 PHP 最为熟悉, 所以从 PHP 开始说起.

PHP

PHP 可以作为其它 webserver (如 apache) 的一个模块工作, 也可以工作在 CLI 模式下. 当然我们这里要说的是 CGI 模式, PHP 有可以以两种方式工作在 CGI 模式下, 一是开箱即用的 php-cgi, 另一是 php-fpm.

php-cgi

php-cgi 是开箱即用的, 当你编译完了 php 源代码, php-cgi 也就编译完了.

php-fpm

php-fpm 是更强大的 FastCGI 实现, 它实际上是包装了 php-cgi, php-cgi 启动后就一个进程, 而 php-fpm 则是能够根据负载量动态的创建/销毁进程 (叫做 workder 进程), 而且在创建这些 workder 进程的时候, 是可以指定不同的用户, 用户组的, 这都是单纯的 php-cgi 不能实现的.
php-fpm 最开始作为一个第三方的实现, 目前已经被包含到 php 核心里了, 现在编译 php 源码, 默认就会编译 php-fpm.

spawn-fcgi + run_php

实际上, 还有第三种方式 — spawn-fcgi, 之所以上面只说 php 有两种运行在 FastCGI 的方式没有包括它, 是因为 spawn-fcgi 并不是专用于 php 的, 它还可以和别的 fastcgi 程序配合使用, 比如可以和 rails 搭配使用.

spawn-fcgi 先前是 lighttpd 的项目, 现在独立了出来, 它的作用就是将别的 fastcgi 程序进程化, 通过它的调用方式你就能猜测到它实际上所干的事:

这是 spawn-fcgi 的 Lighttpd 官网文档里, 启动 php-cgi 的脚本, 脚本的名字叫 run_php:

#!/bin/sh
# Use this as a ./run script with daemontools or runit
# You should replace xxx with the user you want php to run as (and www-data with the user lighty runs as)

exec 2>&1
PHP_FCGI_CHILDREN=2 \
PHP_FCGI_MAX_REQUESTS=1000 \
exec /usr/bin/spawn-fcgi -n -s /var/run/lighttpd/php-xxx.sock -n -u xxx -U www-data -- /usr/bin/php5-cgi

spawn-fcgi 还有一个脚本的名字叫 run_rails, 还有一个叫 run_generic, 你能够猜出它们的作用是什么. 具体可以见参考3.

最后, FastCGI 官网上有关于 php-fpm, php-cgi, spawn-fcgi 的比较, 写的非常好. 可以看一看.

Python

我找了很久, 在 python 里似乎没有直接使用 fastcgi 的案例, 因为 python 有一套自己的与 webserver 通信的协议: WSGI. 当然, 没找到相关的案例不代表 python 不能支持 fastcgi, 其实想要支持 fastcgi 是非常简单的, 几乎任何语言都可以写出一个 fastcgi server 来. 想想说白了, fastcgi 中, fastcgi server 与 webserver 基本也就通过两种方式通信, 一种是 unix socket (win32 中的 named pipe), 一种是 tcp sockets. webserver 以前将客户端的请求信息通过环境变量传给 CGI 脚本, 现在是通过 socket 传给 fastcgi server. 所以说, 任何语言, 只要其编译器/解释器能够支持调用系统的 socket 侦听机制, 还能够较好的解析出 webserver 发给 socket 上的消息, 那么它也就实现了 fastcgi server 的功能.

python 也不例外, 而在 python 中不使用 FastCGI, 是因为 python 有着更适合这们语言的协议, 也就是上面说的 WSGI, 它能够使得在开发 wsgi 应用程序时, 开发者不必处理 HTTP 请求流信息, wsgi 暴露给开发者的直接就是 python 对象, 比如上传文件时, 开发者能够直接取得文件的对象.

python 里确实是有一些框架有实现 fastcgi/scgi 的功能的, 比如 flup, django, 但是他们一般会把 fastcgi 的细节隐藏了, 暴露给开发者的仍然是 wsgi 的接口. 比如下面这个使用 flup 的例子:

#!/usr/bin/env python
# -*- coding: UTF-8 -*-

from cgi import escape
import sys, os
from flup.server.fcgi import WSGIServer

def app(environ, start_response):
    start_response('200 OK', [('Content-Type', 'text/html')])

    yield '<h1>FastCGI Environment</h1>'
    yield '<table>'
    for k, v in sorted(environ.items()):
         yield '<tr><th>%s</th><td>%s</td></tr>' % (escape(k), escape(v))
    yield '</table>'

WSGIServer(app).run()

WSGIServer 这个类的 run() 方法调用后, 就启动了一个 fastcgi server, 但是, 这个类仍然是需要接受一个 callable (上面的 app(environ, start_response) 方法), 这是 wsgi 里的标准接口.

关于 wsgi 的更多内容, 可见本系列的番外篇.

补充一点

python 有个库叫做 fcgi-python, 是方便别人用 python 写 fastcgi server 用的.

[未完待续…]

参考

  1. http://en.wikipedia.org/wiki/FastCGI
  2. FastCGI 官网: http://www.fastcgi.com/drupal/node/2
  3. spawn-fcgi 项目官网: http://redmine.lighttpd.net/projects/spawn-fcgi/wiki
  4. FastCGI 官网上, 关于 php-fpm, php-cgi, spawn-fcgi 的比较, 强烈推荐: http://php-fpm.org/about/
  5. 一篇中文文章, 介绍了 php-cgi, php-fpm, spawn-fcgi, 还不错: http://www.joyphper.net/article/201310/237.html
  6. mod_wsgi 官方的文档, 强烈推荐: http://code.google.com/p/modwsgi/wiki/QuickConfigurationGuide
  7. django 配置 fastcgi 的教程: https://docs.djangoproject.com/en/dev/howto/deployment/fastcgi/
  8. flup 配置 fastcgi 的教程: http://wiki.dreamhost.com/Python_FastCGI
  9. 有关 python 使用 fastcgi 的主题: http://stackoverflow.com/questions/7048057/running-python-through-fastcgi-for-nginx?lq=1

通用网关协议 (CGI) 进化史 (中篇)

CGI 由于前面提到的性能问题, 越来越无法满足大多数网站的要求. 于是, FastCGI 和 Simple CGI 出现了.

Simple CGI (SCGI)

Simple CGI 简称 SCGI, 和 FastCGI 一起, 是为了解决原始 CGI 的性能问题而出现的. 它们的解决方式和 “将脚本程序解释器嵌入 webserver (如 mod_php, mod_python” 不同, 它们的解决方式是创建一个 long-running 的后台进程, 以处理 webserver 的 forward 过来的请求.

当然, webserver 上仍然需要实现 FastCGI 或者 SCGI 协议的, apache 有 mod_fastcgi/mod_fcgid, lighttpd 也有 mod_fastcgi 和 mod_scgi.

SCGI 和 FastCGI 基本是一样的, 除了 SCGI 比 FastCGI 更容易实现 — 正如其名字所暗示的那样.

下面看一下在各个 webserver 对 SCGI 的支持情况与配置方式.

Apache

最初, Apache 的模块 mod_scgi 负责实现 scgi 协议, 这个模块不是 Apache 自己开发的, 似乎是 python 用户组开发的, 因为官网就是 python 站上 (参考1). 这个模块在 Apache 2.0+ 上可用, 是非常稳定的模块. 可是现在其开发似乎停滞了, 可能是因为 Apache 上 SCGI 用的少, 而且 mod_proxy_scgi 模块出来的原因吧.

mod_proxy_scgi 模块是相对较新的模块, 是被包含在 Apache 源代码里的, 内建的模块.

mod_scgi 模块的配置大概如下 (详见 参考5):

# (This actually better set up permanently with the command line
# "a2enmod scgi" but shown here for completeness)
LoadModule scgi_module /usr/lib/apache2/modules/mod_scgi.so

# Set up a location to be served by an SCGI server process
SCGIMount /dynamic/ 127.0.0.1:4000
The deprecated way of delegating requests to an SCGI server is as follows:

<Location "/dynamic">
    # Enable SCGI delegation
    SCGIHandler On
    # Delegate requests in the "/dynamic" path to daemon on local
    # server, port 4000
    SCGIServer 127.0.0.1:4000
</Location>

mod_proxy_scgi 模块的配置大概如下 (详见 参考6):

ProxyPass /scgi-bin/ scgi://localhost:4000/
<Proxy balancer://somecluster>
    BalancerMember scgi://localhost:4000
    BalancerMember scgi://localhost:4001
</Proxy>

前面说了, SCGI 的解决方式是创建一个 long-running 的进程, 或者监听某个 TCP/IP 端口, 或者监听一个 unix 套接字, 以便于和 webserver 通信. 那么现在 webserver 相当于 scgi 客户端, 我们现在还缺少一个 scgi 服务器端.

从上面的 mod_scgi 和 mod_proxy_scgi 的配置也可以看出, SCGI 协议里是还需要一个 scgi 服务器端的.

SCGI 的服务器端是和语言相关的 (与 FastCGI 一样), 不同的语言有不同的实现, 参考8 介绍了这一点.

注: 下面的内容对 FastCGI 也是适用的

如最古老的 CGI 协议一样, SCGI 服务器会将请求递交给它的子进程, 子进程会去执行实际的任务. 不同的地方是, 子进程完成任务后不会退出, 而是 sleep, 等待下一个请求的到来.

SCGI 的另一个好处是, 它不必非得和 webserver 处在同一个机器上.

Lighttpd

Lighttpd 自带着 mod_scgi 模块, 其配置方式与 Lighttpd 的 FastCGI 配置方式一样, 可参考 Lighttpd 的 FastCGI 部分.

Nginx

Nginx 也有自己的 ngx_http_scgi_module 模块以支持 scgi.

参考

  1. SCGI 的官方网站, 不明白为何是放在 python 官网上的: http://python.ca/scgi/
  2. 维基页, 讲得很少: http://en.wikipedia.org/wiki/Simple_Common_Gateway_Interface
  3. 强烈推荐: https://docs.python.org/2/howto/webservers.html
  4. 讲了 Apache 中的两种 scgi 的模块, 推荐: http://alesteska.blogspot.com/2012/07/scgi-in-apache-http-server-and-in.html
  5. 讲了 Apache 中的 mod_scgi 模块的配置: http://quixote.python.ca/scgi.dev/doc/guide.html
  6. Apache 官网上关于 mod_proxy_scgi 的介绍: http://httpd.apache.org/docs/trunk/mod/mod_proxy_scgi.html
  7. http://woof.sourceforge.net/woof-ug/_woof/docs/ug/apache_scgi
  8. 吐血推荐, 不过这文章有四页, 只看前两页即可: http://www.linuxjournal.com/article/9310