面向搜索引擎的内容管理系统(CMS)设计
    ——利用Squid反相代理缓存实现网站内容静态发布

作者: 车东 Email: chedongATbigfoot.com/chedongATchedong.com

写于:2003/03 最后更新:

版权声明:可以任意转载,转载时请务必以超链接形式标明文章原始出处和作者信息及本声明
http://www.chedong.com/tech/cms.html

关键词:"content manage system" cms path_info cgi php cache squid 内容管理 mod_rewrite url rewrite 系统 缓存 Cacheable "Search engine friendly"

内容摘要:
你完全不必耐心的看到最后,本文主要说明的是在设计CMS时以下2点注意事项:

  1. 搜索引擎友好(Search engine Friendly):基于PATH_INFO的参数解析使得动态网页在链接(URI)形式上更像静态的目录结构,大大方便网站内容被搜索引擎收录;
  2. 可缓存性(Cache Friendly):CMS本身不要过多考虑“效率”问题,只要页面输出设计的比较Cacheable,效率问题可以通过更前端专业的缓存服务器解决。

后面附有一个简单的利用PATH_INFO机制 + SquidWEB加速模式实现PHP动态网页的静态发布的例子,比起那些能导出静态页面的大型发布系统这种轻量级的动态缓存解决方案只需对原有的动态发布系统做少量的修改,从而大大提高了原有内容发布系统的重用度。

网站内容静态发布的重要性:Cacheable / Search Engine Friendly

由于一个动态页面的速度往往会比静态页面慢2-10倍,因此对于一个访问量逐步向百万级发展的网站来说,访问速度很快成为一个瓶颈。除了优化内容发布系统的应用本身外,如果能把更新频率比较低的动态页面转存成静态网页来发布,速度上的提升效果将是显著的,而静态网页如果能被缓存在内存里,访问速度更会比原有动态网页有2-3个数量级的提高。

在国外内容管理系统(CMS)已经是一个非常成熟的行业,能够真正支撑大访问的系统中静态页面输出和缓存系统几乎是必须的。

此外随着互联网上的内容以惊人速度的增长也越来越突出了搜索引擎的重要性,如果网站想更好地被搜索引擎收录,网站设计除了面向用户友好(User Friendly)外,面向搜索引擎友好的设计也是非常重要的。链接地址相对固定的静态网页比较适合搜索引擎索引,动态网页后面跟的参数灵活度很大,因此很多搜索引擎都往往会忽略动态页面,比如:对于news.php?day=22&month=03&year=2003,很多搜索引擎可能只索引news.php这个页面一次,更多其他参数的页面可能都会当成相似内容滤掉;我个人一直怀疑在搜索引擎中:即使是同样内容,静态页面往往也比动态网页的PageRank高。

因此,将动态页面转换成静态页面,无论从效率上还是面向搜索引擎友好上,都是一个门户级内容发布系统必须面对的问题。

静态缓存和动态缓存的比较

静态页面的缓存可能有2种形式:

  1. 静态缓存:是在新内容发布的同时就立刻生成相应内容的静态页面,比如:2003年3月22日,管理员通过后台内容管理界面录入一篇新闻后,就立刻生成http://www.chedong.com/tech/2003/03/22/001.html这个静态页面,并同步更新http://www.chedong.com/tech/index.html这个静态页面上的相关链接。
  2. 动态缓存:是在新内容发布以后,并不预先生成相应的静态页面,直到对相应内容发出请求时,如果前台缓存服务器找不到相应缓存,就向后台内容管理服务器发出请求,后台系统会生成相应内容的静态页面,用户第一次访问页面时可能会慢一点,但是以后就是直接访问缓存了。
    如果去ZDNet等国外网站会发现他们使用的基于Vignette内容管理系统都有这样的页面名称:0,22342566,300458.html。其实这里的0,22342566,300458就是用逗号分割开的多个参数:
    第一次访问找不到页面后,相当于会在服务器端产生一个doc_type=0&doc_id=22342566&doc_template=300458的查询,
    而查询结果会生成的缓存的静态页面:0,22342566,300458.html

静态缓存的缺点:

在动态缓存模式中,内容管理系统只需要关心各个页面自身,而相关的其他页面链接能自动更新,从而大大减少了设计触发器设计的需要。

VIGNETTE的动态缓存虽然很好,但是一个系统如果设计得太全面其实也是有很大危险的:如果一个频道下文章很多:比如达到十万时,如果生成的静态页面都在一个目录下,对系统文件系统是一个极大的危害,因为一个目录下文件个数超过3000效率就会非常差,甚至连rm *时都会出现too many arguments错误。

简单的说,一个好的内容管理系统应该包括:
  1. 输入:方便的内容录入,所见即所得的编辑界面,权限控制等……
  2. 输出:方便的模板管理,缓存发布等……
设计或寻找这样一个系统如果考虑功能全面和高集成度,你会发现只有那些几十万$以上的专业内容发布系统才能你满足所有的需求。

以前做应用的时候也用过一些方式:应用首次访问以后将生成的内容存成一个缓存文件,下次请求时从缓存目录中读取缓存文件,内容更新时,应用把内容从缓存目录中删掉,从而减少对数据库的访问。虽然这样做也能承载比较大的负载,但这样的内容管理和缓存一体的系统是很难分离的。

如果换一个思路:通过一定的分工现内容管理和缓存机制2者的分离,你会发现无论哪一方面可选的余地都是非常大的。甚至有可能利用目前的已经是“功能”比较全面的内容管理系统,而让所有“效率”问题都由前台更专业,而且是很容易分布的缓存服务器解决:可以是通过开放源代码的SQUID做反相代理的WEB加速,可以是专门的缓存硬件设备,甚至是专业的缓存服务商。

动态缓存必须有一个基于静态链接本身的参数解析过程,很多专业内容管理系统系统都是将参数解析机制做成了WEB服务器的模块实现的。

我们可以把以前的HTTP/GET方式的?key=value改为直接用/value1/value2的方式来传递,从而实现了动态页面的静态URL形式。而缓存只需要在前端加上一层CACHE服务器,比如:Squid。网站动态内容的动态缓存发布就可以实现了。

按照这个机制实现的发布系统很好地体现了应用系统的分工:把复杂地内容管理系统分解成:内容输入和缓存这2个相对简单的系统实现。而中间的内容发布通过URL REWRITE或PATH_INFO解决动态页面的参数传递:

______________________             ___________________
|Squid Software cache| |F5 Hardware cache|
---------------------- -------------------
\ /
\ ________________ /
|ASP |JSP |PHP |
PATH_INFO Based Content Manage System
----------------

把URI地址用作参数传递:URL REWRITE和PATH_INFO

最近看到很多关于面向搜索引擎URL设计优化(URI Pretty)的文章,提到了很多利用一定机制将动态网页参数变成像静态网页的形式:
比如可以将:http://www.chedong.com/phpMan.php?mode=man&parameter=ls
变成:http://www.chedong.com/phpMan.php/man/ls

最简单的是基于各种WEB服务器中的URL重写转向(Rewrite)模块的URL转换:这样几乎可以不修改程序的实现将news.asp?id=234的映射成news/234.html

Apache上有一个模块(非缺省):mod_rewrite:当然URL REWRITE的强大功能还远远不止于此。

当我需要将将news.asp?id=234的映射成news/234.html时:只需设置:
RewriteRule /news/(\d+)\.html /news\.asp\?id=$1 [N,I]
这样就把 /news/234.html 映射成了 /news.asp?id=234
当有对/news/234.html的请求时:web服务器会把实际请求转发给/news.asp?id=234

而在IIS也有相应的REWRITE模块:比如ISAPI REWRITEIIS REWRITE,语法都是基于正则表达式,因此语法是几乎相同的:

比对于某一个简单应用可以是:
RewriteRule /news/(\d+)? /news\.asp\?id=$1 [N,I]
这样就把 /news/234 映射到了 /news.asp?id=234

如我需要把 http://www.myhost.com/foo.php?a=A&b=B&c=C 表现成 http://www.myhost.com/foo.php/a/A/b/B/c/C。而一个更通用的能够将所有的动态页面进行参数映射的表达式是:
RewriteRule (.*?\.php)(\?[^/]*)?/([^/]*)/([^/]*)(.+?)? $1(?2$2&:\?)$3=$4?5$5: [N,I]

通过URL REWRITE还有一个好处就是隐藏后台实现:
比如我们需要将应用从news.asp?id=234迁移成news.php?query=234时,前台的表现可以一直保持为news/234.html。从实现应用和前台表现的分离:保持了URL的稳定性,在实现后台应用平台的迁移时非常有用。使用mod_rewrite甚至可以把请求转发到其他后台服务器上:


另外一个方式就是基于PATH_INFO:
PATH_INFO是一个CGI 1.1的标准,所有直接跟在CGI或动态页面app.cgi后面的"/value_1/value_2"就是PATH_INFO参数:
比如http://www.chedong.com/phpMan.php/man/ls,中:$PATH_INFO = "/man/ls"

PATH_INFO是CGI标准,因此PHP Servlet等都有比较好的支持。 比如Servlet中就有request.getPathInfo()方法。
注意:/myapp/servlet/Hello/foo的getPathInfo()返回的是/foo,而/myapp/dir/hello.jsp/foo的getPathInfo()将返回的/hello.jsp,从这里你也可以知道jsp其实就是一个Servlet的PATH_INFO参数。ASP不支持PATH_INFO,

PHP中基于PATH_INFO的参数解析的例子如下:
//注意:第一个参数是空的,参数按"/"分割
if ( isset($_SERVER["PATH_INFO"]) ) {
    list($nothing, $day, $id) = explode('/', $_SERVER["PATH_INFO"]);
}

如何隐蔽应用:例如.php,的扩展名:
在APACHE中这样配置:
<FilesMatch "^app_name$">
    ForceType application/x-httpd-php
</FilesMatch>

如何更像静态页面:app_name/my/app.html
解析的PATH_INFO参数的时候,把最后一个参数的最后5个字符“.html”截断即可。
注意:APACHE2中缺省是不允许PATH_INFO的,需要设置
AcceptPathInfo on

特别是针对使用虚拟主机用户,无权安装和配置mod_rewrite的时候,PATH_INFO往往就成了唯一的选择。

虽然通过修改设置SQUID也可以对带?的动态页面进行缓存,但为了方便搜索引擎收录索引,还是将参数改成PATH_INFO比较好。

OK,这样以后看见类似于http://www.example.com/article/234这样的网页你就知道其实是article/show.php?id=234这个php程序生成的动态网页,很多站点表面看上去可能有很多静态目录,其实很有可能都是使用1,2个程序实现的内容发布。比如很多WIKIWIKI系统都使用了这个机制:整个系统就一个简单的wiki程序,而看上去的目录其实都是这个应用拿后面的地址作为参数的查询结果。

利用基于MOD_REWRITE/PATH_INFO + CACHE服务器的解决方案对原有的动态发布系统进行改造,也可以大大降低旧有系统升级到新的内容管理系统的成本。

面向缓存的页面设计

让页面能够比较好的被缓存服务器缓存,必须在产生内容的WEB服务器上设置,让返回内容的HTTP HEADER中加入"Last-Modified"和"Expires"声明,比如:
Last-Modified: Wed, 14 May 2003 13:06:17 GMT
Expires: Fri, 13 Jun 2003 13:06:17 GMT
以允许前端SQUID服务器缓存:
ASP应用的缓存设计:
首先在公用的包含文件中(比如include.asp)加入以下公用函数:
<%
' Converts date (19991022 11:08:38) to http form (Fri, 22 Oct 1999 12:08:38 GMT)
Function DateToHTTPDate(ByVal OleDATE)
  Const GMTdiff = #08:00:00#
  OleDATE = OleDATE - GMTdiff
  DateToHTTPDate = engWeekDayName(OleDATE) & _
    ", " & Right("0" & Day(OleDATE),2) & " " & engMonthName(OleDATE) & _
    " " & Year(OleDATE) & " " & Right("0" & Hour(OleDATE),2) & _
    ":" & Right("0" & Minute(OleDATE),2) & ":" & Right("0" & Second(OleDATE),2) & " GMT"
End Function

Function engWeekDayName(dt)
    Dim Out
    Select Case WeekDay(dt,1)
        Case 1:Out="Sun"
        Case 2:Out="Mon"
        Case 3:Out="Tue"
        Case 4:Out="Wed"
        Case 5:Out="Thu"
        Case 6:Out="Fri"
        Case 7:Out="Sat"
    End Select
    engWeekDayName = Out
End Function

Function engMonthName(dt)
    Dim Out
    Select Case Month(dt)
        Case 1:Out="Jan"
        Case 2:Out="Feb"
        Case 3:Out="Mar"
        Case 4:Out="Apr"
        Case 5:Out="May"
        Case 6:Out="Jun"
        Case 7:Out="Jul"
        Case 8:Out="Aug"
        Case 9:Out="Sep"
        Case 10:Out="Oct"
        Case 11:Out="Nov"
        Case 12:Out="Dec"
    End Select
    engMonthName = Out
End Function
%>

然后在具体的页面中,比如index.asp和news.asp的“最上面”加入以下代码:HTTP Header

<!--#include file="../include.asp"-->
<%
' set Page Last-Modified Header:
' Converts date (19991022 11:08:38) to http form (Fri, 22 Oct 1999 12:08:38 GMT)
Response.AddHeader "Last-Modified", DateToHTTPDate(Now())

' The Page Expires in Minutes
Response.Expires = 60

' Set cache control to externel applications
Response.CacheControl = "public"
%>

其中Response.Expires 是设置页面过期时间的:单位是分钟

如何检查目前站点页面的可缓存性(Cacheablility)呢?可以参考以下2个站点上的工具:
http://www.ircache.net/cgi-bin/cacheability.py

面向缓存的站点规划

一个利用SQUID的Transparent对多个站点进行做WEB加速http acceleration方案:

原先一个站点的规划可能是这样的:
200.200.200.207 www.chedong.com
200.200.200.208 news.chedong.com
200.200.200.209 bbs.chedong.com
200.200.200.205 images.chedong.com

在SQUID模式下:所有站点都通过外部DNS指向到同一个IP:200.200.200.200/201这2台SQUID缓存服务器上(使用2台是为了冗余备份)
                          _____________________   ________
www.chedong.com 请求 \ | Squid cache box | | | / 192.168.0.4 www.chedong.com
news.chedong.com 请求 -| 200.200.200.200/201 |-|firewall| - 192.168.0.4 news.chedong.com
bbs.chedong.com 请求 / | /etc/hosts | | box | \ 192.168.0.3 bbs.chedong.com
--------------------- --------
编译和配置过程:
  1. ./configure --enable-referer-log --disable-internal-dns
    --disable-internal-dns:禁用SQUID的DNS解析
    --enable-referer-log:便于APACHE COMBINED格式日志生成

  2. 配置:
    http_port 80
    httpd_accel_host virtual
    httpd_accel_port 8000
    httpd_accel_uses_host_header on

    # accelerater my domain only
    acl acceleratedHosts dstdom_regex chedong.com
    # accelerater http protocol on port 80
    acl acceleratedProtocol protocol HTTP
    acl acceleratedPort port 80
    # access arc
    acl all src 0.0.0.0/0.0.0.0

    # Allow requests when they are to the accelerated machine AND to the
    # right port with right protocol
    http_access allow acceleratedProtocol acceleratedPort acceleratedHosts
    http_access allow all
在/etc/hosts中:加入内部的DNS解析,比如:
192.168.0.4 www.chedong.com
192.168.0.4 news.chedong.com
192.168.0.3 bbs.chedong.com

工作原理:
SQUID服务器上关闭了DNS解析,这样,请求外部过来时,设置SQUID根据/etc/hosts文件进行内部DNS解析。这样,服务器请求就可以转发到我们指定的内部地址上。

使用SQUID的反相代理加速,我们不仅可以得到性能上的提升,而且还能获得额外的安全性和配置的灵活度:
小节:

附1:SQUID性能测试试验

phpMan.php是一个基于php的man page server,每个man page需要调用后台的man命令和很多页面格式化工具,系统负载比较高,提供了Cache Friendly的URL,以下是针对同样的页面的性能测试资料:
测试环境:Redhat 8 on Cyrix 266 / 192M Mem
测试程序:使用apache的ab(apache benchmark):
测试条件:请求50次,并发50个连接
测试项目:直接通过apache 1.3 (80端口) vs squid 2.5(8000端口:加速80端口)

测试1:无CACHE的80端口动态输出:
ab -n 100 -c 10 http://www.chedong.com:81/phpMan.php/man/kill/1
This is ApacheBench, Version 1.3d <$Revision: 1.58 $> apache-1.3
Copyright (c) 1996 Adam Twiss, Zeus Technology Ltd, http://www.zeustech.net/
Copyright (c) 1998-2001 The Apache Group, http://www.apache.org/

Benchmarking localhost (be patient).....done
Server Software:        Apache/1.3.23                                     
Server Hostname:        localhost
Server Port:            80

Document Path:          /phpMan.php/man/kill/1
Document Length:        4655 bytes

Concurrency Level:      5
Time taken for tests:   63.164 seconds
Complete requests:      50
Failed requests:        0
Broken pipe errors:     0
Total transferred:      245900 bytes
HTML transferred:       232750 bytes
Requests per second:    0.79 [#/sec] (mean)
Time per request:       6316.40 [ms] (mean)
Time per request:       1263.28 [ms] (mean, across all concurrent requests)
Transfer rate:          3.89 [Kbytes/sec] received

Connnection Times (ms)
              min  mean[+/-sd] median   max
Connect:        0    29  106.1      0   553
Processing:  2942  6016 1845.4   6227 10796
Waiting:     2941  5999 1850.7   6226 10795
Total:       2942  6045 1825.9   6227 10796

Percentage of the requests served within a certain time (ms)
  50%   6227
  66%   7069
  75%   7190
  80%   7474
  90%   8195
  95%   8898
  98%   9721
  99%  10796
 100%  10796 (last request)

测试2:SQUID缓存输出
/home/apache/bin/ab -n50 -c5 "http://localhost:8000/phpMan.php/man/kill/1"
This is ApacheBench, Version 1.3d <$Revision: 1.58 $> apache-1.3
Copyright (c) 1996 Adam Twiss, Zeus Technology Ltd, http://www.zeustech.net/
Copyright (c) 1998-2001 The Apache Group, http://www.apache.org/

Benchmarking localhost (be patient).....done
Server Software:        Apache/1.3.23                                     
Server Hostname:        localhost
Server Port:            8000

Document Path:          /phpMan.php/man/kill/1
Document Length:        4655 bytes

Concurrency Level:      5
Time taken for tests:   4.265 seconds
Complete requests:      50
Failed requests:        0
Broken pipe errors:     0
Total transferred:      248043 bytes
HTML transferred:       232750 bytes
Requests per second:    11.72 [#/sec] (mean)
Time per request:       426.50 [ms] (mean)
Time per request:       85.30 [ms] (mean, across all concurrent requests)
Transfer rate:          58.16 [Kbytes/sec] received

Connnection Times (ms)
              min  mean[+/-sd] median   max
Connect:        0     1    9.5      0    68
Processing:     7    83  537.4      7  3808
Waiting:        5    81  529.1      6  3748
Total:          7    84  547.0      7  3876

Percentage of the requests served within a certain time (ms)
  50%      7
  66%      7
  75%      7
  80%      7
  90%      7
  95%      7
  98%      8
  99%   3876
 100%   3876 (last request)

结论:No Cache / Cache = 6045 / 84 = 70
结论:对于可能被缓存请求的页面,服务器速度可以有2个数量级的提高,因为SQUID是把缓存页面放在内存里的(因此几乎没有硬盘I/O操作)。

附2:一个CACHE多主机APACHE服务的SQUID安装配置:

squid的编译:
./configure --enable-useragent-log  --enable-referer-log --enable-default-err-language=Simplify_Chinese --enable-err-languages="Simplify_Chinese English" --disable-internal-dns 
make
#make install
#cd /usr/local/squid
make dir cache
chown squid.squid *
vi /usr/local/squid/etc/squid.conf

---------------------cut here----------------------------------
# visible name
visible_hostname cache.example.com

# cache config: space use 1G and memory use 256M
cache_dir ufs /usr/local/squid/cache 1024 16 256
cache_mem 256 MB
cache_effective_user squid
cache_effective_group squid


http_port 80
httpd_accel_host virtual
httpd_accel_single_host off
httpd_accel_port 80
httpd_accel_uses_host_header on
httpd_accel_with_proxy on

# accelerater my domain only
acl acceleratedHostA dstdomain .example1.com
acl acceleratedHostB dstdomain .example2.com
acl acceleratedHostC dstdomain .example3.com
# accelerater http protocol on port 80
acl acceleratedProtocol protocol HTTP
acl acceleratedPort port 80
# access arc
acl all src 0.0.0.0/0.0.0.0

# Allow requests when they are to the accelerated machine AND to the
# right port with right protocol
http_access allow acceleratedProtocol acceleratedPort acceleratedHostA
http_access allow acceleratedProtocol acceleratedPort acceleratedHostB
http_access allow acceleratedProtocol acceleratedPort acceleratedHostC

# logging
emulate_httpd_log on
referer_log /usr/local/squid/var/logs/referer.log
useragent_log /usr/local/squid/var/logs/agent.log


----------------------cut here---------------------------------

创建缓存目录:
/usr/local/squid/sbin/squid -z

启动squid
/usr/local/squid/sbin/squid

停止squid:
/usr/local/squid/sbin/squid -k shutdown

启用新配置:
/usr/local/squid/sbin/squid -k reconfig

通过crontab每天0点截断/轮循日志:
0 0 * * * (/usr/local/squid/sbin/squid -k rotate)

附3:如何在IIS上利用PHP支持PATH_INFO

PHP的ISAPI模式安装备忘:只试成 php-4.2.3-Win32

解包目录
========
php-4.2.3-Win32.zip c:\php

PHP.INI初始化文件
=================
复制:c:\php\php.ini-dist 到 c:\winnt\php.ini

配置文件关联
============
按照install.txt中的说明配置文件关联

运行库文件
==========
复制 c:\php\php4ts.dll 到 c:\winnt\system32\php4ts.dll

这样运行后:会发现php把PATH_INFO映射到了物理路径上
Warning: Unknown(C:\CheDong\Downloads\ariadne\www\test.php\path): failed to create stream: No such file or directory in Unknown on line 0

Warning: Unknown(): Failed opening 'C:\CheDong\Downloads\ariadne\www\test.php\path' for inclusion (include_path='.;c:\php4\pear') in Unknown on line 0

安装ariadne的PATCH
==================
停止IIS服务
net stop iisadmin
ftp://ftp.muze.nl/pub/ariadne/win/iis/php-4.2.3/php4isapi.dll
覆盖原有的c:\php\sapi\php4isapi.dll

注:ariadne是一个基于PATH_INFO的内容发布系统

PHP 4.3.2 RC2中CGI模式的PATH_INFO已经修正,照常安装即可。

参考资料:

CMS行业观察
http://www.cmswatch.com

CMS讨论邮件列表
http://www.cms-list.org

一个基于PATH_INFO的开源内容管理系统
http://typo3.com/

商业的和开源CMS项目列表:
http://directory.google.com/Top/Computers/Software/Internet/Site_Management/Content_Management/

搜索引擎友好的URL设计
http://www.sitepoint.com/article/485
说不定这个URL原来就是articel.php?id=485

HTTP代理缓存
http://vancouver-webpages.com/proxy.html

可缓存的页面设计
http://linux.oreillynet.com/pub/a/linux/2002/02/28/cachefriendly.html

相关RFC文档:

可缓存性检查:
http://www.web-caching.com/cacheability.html

URL Rewrite文档:
http://www.isapirewrite.com/docs/
http://httpd.apache.org/docs/mod/mod_rewrite.html
http://httpd.apache.org/docs-2.0/mod/mod_rewrite.html

原文出处:<a href="http://www.chedong.com/tech/cms.html">http://www.chedong.com/tech/cms.html</a>
<<返回