大批量虚拟主机的动态配置

本文档描述如何使用Apache有效的架设大批量虚拟主机。

动机

如果你的配置文件httpd.conf中包含类似下面的许多<VirtualHost>段,并且其中的内容都大致相同的话,你应该会对这里所讲的技术感兴趣。比如:

NameVirtualHost 111.22.33.44

<VirtualHost 111.22.33.44>

 ServerName                 www.customer-1.com

    DocumentRoot        /www/hosts/www.customer-1.com/docs

    ScriptAlias  /cgi-bin/  /www/hosts/www.customer-1.com/cgi-bin 
</VirtualHost>

<VirtualHost 111.22.33.44>

 ServerName                 www.customer-2.com

    DocumentRoot        /www/hosts/www.customer-2.com/docs

    ScriptAlias  /cgi-bin/  /www/hosts/www.customer-2.com/cgi-bin 
</VirtualHost>

# 等 等 等 。。。 

<VirtualHost 111.22.33.44>

 ServerName                 www.customer-N.com

    DocumentRoot        /www/hosts/www.customer-N.com/docs

    ScriptAlias  /cgi-bin/  /www/hosts/www.customer-N.com/cgi-bin 
</VirtualHost>

最基本的思想是用动态的机制来实现所有这些静态的<VirtualHost>配置段。这样做有许多优点:

  1. 配置文件变小,使得Apache可以更快的启动,同时消耗更少的内存。
  2. 添加一个虚拟主机,应该只是简单的在文件系统中创建合适的目录,以及配置相关的DNS信息,且无需重新启动Apache 。

主要的缺点是你无法针对每个虚拟主机使用不同的日志文件。然而,如果真的在配置有大量虚拟主机的服务器上记录不同的日志文件的话,很有可能会达到操作系统所允许的最大文件描述符的数量。更好的办法是把日志写到管道或者先入先出的栈,并启用其他的进程来分拣所得到的日志信息(同时也可以做一些历史纪录的统计等等)。

概述

一个虚拟主机由两部分来定义:一个是它的IP地址,还有一个是HTTP的"Host:"请求头。动态大量虚拟主机的技术,是基于自动在所要返回的文件路径中插入相关信息的想法实现的。使用mod_vhost_alias可以很容易的实现,但如果你的Apache版本低于1.3.6 ,则你必须使用mod_rewrite 。两者在默认情况下都不启用;要使用他们,必须在配置和编译Apache的阶段启用。

我们需要做很多"伪装",才能使动态虚拟主机看起来像普通主机。最重要的一点是Apache使用虚拟主机名(ServerName)来生成自引用(self-referential)URL等信息。这是用ServerName指令来配置的,并且可以通过环境变量SERVER_NAME传递给CGI脚本。运行时实际使用的值是由UseCanonicalName指令的设置来控制的。当 UseCanonicalName Off 时,虚拟主机名(ServerName)取自请求中的"Host:"头。当 UseCanonicalName DNS 时,则通过DNS反解析虚拟主机的IP地址得到主机名。以前的做法是基于名称的动态虚拟主机,现在常用基于IP地址的虚拟主机。如果Apache无法判断虚拟主机名,则可能是没有"Host:"头或是DNS解析失败,这样种情况下,Apache将使用配置ServerName时所填写的主机名。

另一件需要"伪装"的事情是文档根目录(由DocumentRoot配置并可以通过DOCUMENT_ROOT环境变量为CGI脚本所使用)。在通常的配置方式下,这些设置信息由核心(core)模块在将URI映射到文件系统的时候使用,但是如果使用动态虚拟主机配置,这些信息将由另外一个使用不同于核心(core)模块将URI映射到文件系统的方式的模块(mod_vhost_aliasmod_rewrite)使用。这两个模块都不负责设置DOCUMENT_ROOT环境变量,所以如果CGI或SSI程序使用了DOCUMENT_ROOT环境变量,那么将得到错误的值。

简单的动态虚拟主机

这是httpd.conf文件中,完成和上文动机部分所提到的虚拟主机一样效果的配置方法,但这里采用了mod_vhost_alias模块:

# 从"Host:"头中取得主机名

UseCanonicalName Off

# 这种日志格式可以从第一个字段中提取出主机名

LogFormat "%V %h %l %u %t \"%r\" %s %b" vcommon

CustomLog logs/access_log vcommon

# 在返回请求的文件名路径中包含主机名

VirtualDocumentRoot /www/hosts/%0/docs

VirtualScriptAlias  /www/hosts/%0/cgi-bin

UseCanonicalName Off 的配置改为 UseCanonicalName DNS 即可实现基于IP地址的虚拟主机。而在文件路径中所要插入的服务器名则通过虚拟主机的IP地址解析得到。

一个实际的个人主页系统

这里对上面的系统作了一点调整,便可作为ISP的个人主页服务器。我们使用了略微复杂的方法,从主机名(ServerName)中提取子字符串,并插入到文件路径中。在这个例子中www.user.isp.com的文档将在/home/user/中定位。并对所有虚拟主机使用单个cgi-bin目录。

# 所有之前的准备事项和上面一样,然后在文件路径中包含主机名

VirtualDocumentRoot /www/hosts/%2/docs

# 单个cgi-bin目录

ScriptAlias  /cgi-bin/  /www/std-cgi/

更复杂的关于VirtualDocumentRoot的设置,可以查阅mod_vhost_alias文档。

在同一个服务器上架设多个主机的虚拟系统

更复杂的设置,应该使用Apache的<VirtualHost>容器来管理各种虚拟主机配置的作用域。例如,你可以用一个IP地址来给个人主页客户使用,同时用下面的配置提供给商业客户使用。自然的,这两者通过运用<VirtualHost>结合到一起。

UseCanonicalName Off

LogFormat "%V %h %l %u %t \"%r\" %s %b" vcommon

<Directory /www/commercial>

 Options FollowSymLinks

    AllowOverride All 
</Directory>

<Directory /www/homepages>

 Options FollowSymLinks

    AllowOverride None 
</Directory>

<VirtualHost 111.22.33.44>

 ServerName www.commercial.isp.com

    CustomLog logs/access_log.commercial vcommon

    VirtualDocumentRoot /www/commercial/%0/docs

    VirtualScriptAlias  /www/commercial/%0/cgi-bin 
</VirtualHost>

<VirtualHost 111.22.33.45>

 ServerName www.homepages.isp.com

    CustomLog logs/access_log.homepages vcommon

    VirtualDocumentRoot /www/homepages/%0/docs

    ScriptAlias         /cgi-bin/ /www/std-cgi/ 
</VirtualHost>

更为有效的基于IP地址的虚拟主机

第一个例子中说过,转为基于IP地址的虚拟主机设置很容易做到。但不幸的是,那种做法并不高效,因为这样会在每次处理请求时,需要查询DNS。通过在文件系统中包含IP地址的做法可以避免这样的问题。这样一来,免去了和主机名的关联,在日志记录中也一样可以用IP来分离不同日志。Apache将不会为了确定主机名(ServerName)而去做DNS查询。

# 从IP地址反解析得到主机名

UseCanonicalName DNS

# 在日志中包含IP地址,便于以后分拣

LogFormat "%A %h %l %u %t \"%r\" %s %b" vcommon

CustomLog logs/access_log vcommon

# 在文件路径中包含IP地址

VirtualDocumentRootIP /www/hosts/%0/docs

VirtualScriptAliasIP  /www/hosts/%0/cgi-bin

使用老版本的Apache

上面的例子基于mod_vhost_alias ,但它是在版本1.3.6之后才出现的。如果你的版本比较老,可以通过使用mod_rewrite来达到相同的目的,如下所示。但只能是基于"Host:"头方式的虚拟主机。

此外还须注意日志方面的问题。Apache1.3.6是第一个支持"%V"日志格式指令的版本,在版本1.3.0-1.3.3中,"%v"选项做和"%V"一样的事情;而在版本1.3.4中没有等价指令。在所有的这些版本中,指令UseCanonicalName可以出现在.htaccess文件中,这意味着客户的设置可能会导致日志记录紊乱。所以最好的做法是使用"%{Host}i"指令,它可以直接记录"Host:"头;注意,这样可能在末尾包含":port",而使用"%V"则不会这样。

使用mod_rewrite实现简单的动态虚拟主机

这里的例子摘自httpd.conf ,效果等同于第一个例子中的情况。前半部分和上面的例子大致相似,只是为了向后兼容mod_rewrite作了适当修改;后半部分配置mod_rewrite来做实际的工作。

有些特别的地方需要注意:默认情况下,mod_rewrite在所有其他URI转换模块(mod_alias等)之前运行,所以如果使用这些模块的话,mod_rewrite必须作相应的调整。同时,我们还要为每个动态虚拟主机变些戏法,使之等效于ScriptAlias

# 从"Host:"头获取主机名

UseCanonicalName Off

# 可分拣的日志

LogFormat "%{Host}i %h %l %u %t \"%r\" %s %b" vcommon

CustomLog logs/access_log vcommon

<Directory /www/hosts>

 # 这里需要ExecCGI ,因为我们不能强制CGI以与ScriptAlias相同的方式执行

    Options FollowSymLinks ExecCGI 
</Directory>

# 接下来是关键部分

RewriteEngine On

# 来自"Host:"头的ServerName ,可能大小写混杂

RewriteMap  lowercase  int:tolower

## 首先处理普通文档

# 允许变名/icons/起作用,其他变名类同

RewriteCond  %{REQUEST_URI}  !^/icons/

# 允许CGI

RewriteCond  %{REQUEST_URI}  !^/cgi-bin/

# 开始"变戏法"

RewriteRule  ^/(.*)$  /www/hosts/${lowercase:%{SERVER_NAME}}/docs/$1

## 现在处理CGI(我们需要强制使用一个MIME类型)

RewriteCond  %{REQUEST_URI}  ^/cgi-bin/

RewriteRule  ^/(.*)$  /www/hosts/${lowercase:%{SERVER_NAME}}/cgi-bin/$1  [T=application/x-httpd-cgi]

# ok 了!

使用mod_rewrite的个人主页系统

这里的配置完成和第二个例子相同的工作。

RewriteEngine on

RewriteMap   lowercase  int:tolower

# 允许CGI工作

RewriteCond  %{REQUEST_URI}  !^/cgi-bin/

# 检查hostname正确与否,之后才能使RewriteRule起作用

RewriteCond  ${lowercase:%{SERVER_NAME}}  ^www\.[a-z-]+\.isp\.com$

# 将虚拟主机名字连接到URI的开头

# [C]表明本次重写的结果将在下一个rewrite规则中使用

RewriteRule  ^(.+)  ${lowercase:%{SERVER_NAME}}$1  [C]

# 现在创建实际的文件名

RewriteRule  ^www\.([a-z-]+)\.isp\.com/(.*) /home/$1/$2

# 定义全局CGI目录

ScriptAlias  /cgi-bin/  /www/std-cgi/

使用独立的虚拟主机配置文件

这样的布局利用了mod_rewrite的高级特性,在独立的虚拟主机配置文件中转换。如此可以更为灵活,但需要较为复杂的设置。

vhost.map文件包含了类似下面的内容:

www.customer-1.com  /www/customers/1

www.customer-2.com  /www/customers/2

# ...

www.customer-N.com  /www/customers/N

http.conf包含了:

RewriteEngine on

RewriteMap   lowercase  int:tolower

# 定义映射文件

RewriteMap   vhost      txt:/www/conf/vhost.map

# 和上面的例子一样,处理别名

RewriteCond  %{REQUEST_URI}               !^/icons/

RewriteCond  %{REQUEST_URI}               !^/cgi-bin/

RewriteCond  ${lowercase:%{SERVER_NAME}}  ^(.+)$

# 这里做基于文件的重新映射

RewriteCond  ${vhost:%1}                  ^(/.*)$

RewriteRule  ^/(.*)$                      %1/docs/$1

RewriteCond  %{REQUEST_URI}               ^/cgi-bin/

RewriteCond  ${lowercase:%{SERVER_NAME}}  ^(.+)$

RewriteCond  ${vhost:%1}                  ^(/.*)$

RewriteRule  ^/(.*)$                      %1/cgi-bin/$1