7.磁盘缓存基础
7.1 cache_dir指令
cache_dir指令是.conf配置文件里最重要的指令之一。它告诉以何种方式存储cache文件到磁盘的什么位置。cache_dir指令取如下参数:
cache_dir scheme directory size L1 L2 [options]
7.1.1 参数:Scheme
Squid支持许多不同的存储机制。默认的(原始的)是ufs。依赖于操作系统的不同,你可以选择不同的存储机制。在./configure时,你必须使用--enable-storeio=LIST选项来编译其他存储机制的附加代码。我将在8.7章讨论aufs,diskd,coss和null。现在,我仅仅讨论ufs机制,它与aufs和diskd一致。
7.1.2 参数:Directory
该参数是文件系统目录,将cache对象文件存放在这个目录下。正常的,cache_dir使用整个文件系统或磁盘分区。它通常不介意是否在单个文件系统分区里放置了多个cache目录。然而,我推荐在每个物理磁盘中,仅仅设置一个cache目录。例如,假如你有2个无用磁盘,你可以这样做:
# newfs /dev/da1d
# newfs /dev/da2d
# mount /dev/da1d /cache0
# mount /dev/da2d /cache1
然后在.conf里增加如下行:
cache_dir ufs /cache0 7000 16 256
cache_dir ufs /cache1 7000 16 256
假如你没有空闲硬盘,当然你也能使用已经存在的文件系统分区。选择有大量空闲空间的分区,例如/usr或/var,然后在下面创建一个新目录。例如:
# mkdir /var/cache
然后在.conf里增加如下一行:
cache_dir ufs /var/cache 7000 16 256
7.1.3 参数:Size
该参数指定了cache目录的大小。这是能使用的cache_dir目录的空间上限。计算出合理的值也许有点难。你必须给临时文件和swap.state日志,留出足够的自由空间(见13.6章)。我推荐挂载空文件系统,可以运行df:
% df -k
Filesystem 1K-blocks Used Avail Capacity Mounted on
/dev/da1d 3037766 8 2794737 0% /cache0
/dev/da2d 3037766 8 2794737 0% /cache1
这里你可以看到文件系统有大约2790M的可用空间。记住,UFS保留了部分最小自由空间,这里约是8%,这就是为什么不能使用全部3040M空间的原因。
你也许试图分配2790M给cache_dir。如果cache不很繁忙,并且你经常轮转日志,那么这样做也许可行。然而,为安全起见,我推荐保留10%的空间。这些额外的空间用于存放的swap.state文件和临时文件。
注意cache_swap_low指令也影响了使用多少空间。我将在7.2章里讨论它的上限和下限。
底线是,你在初始时应保守的估计cache_dir的大小。将cache_dir设为较小的值,并允许写满cache。在运行一段时间后,cache目录会填满,这样你可以重新评估cache_dir的大小设置。假如你有大量的自由空间,就可以轻松的增加cache目录的大小了。
7.1.3.1 Inodes
Inodes(i节点)是unix文件系统的基本结构。它们包含磁盘文件的信息,例如许可,属主,大小,和时间戳。假如你的文件系统运行超出了i节点限制,就不能创造新文件,即使还有空间可用。超出i节点的系统运行非常糟糕,所以在运行之前,你应该确认有足够的i节点。
创建新文件系统的程序(例如,newfs或mkfs)基于总空间的大小,保留了一定数量的i节点。这些程序通常允许你设置磁盘空间的i节点比率。例如,请阅读newfs和mkfs手册的-i选项。磁盘空间对i节点的比率,决定了文件系统能实际支持的文件大小。大部分unix系统每4KB创建一个i节点,这对通常是足够的。研究显示,对大部分cache代理,实际文件大小大约是10KB。你也许能以每i节点8KB开始,但这有风险。
你能使用df -i命令来监视系统的i节点,例如:
% df -ik
Filesystem 1K-blocks Used Avail Capacity iused ifree %iused Mounted on
/dev/ad0s1a 197951 57114 125001 31% 1413 52345 3% /
/dev/ad0s1f 5004533 2352120 2252051 51% 129175 1084263 11% /usr
/dev/ad0s1e 396895 6786 358358 2% 205 99633 0% /var
/dev/da0d 8533292 7222148 628481 92% 430894 539184 44% /cache1
/dev/da1d 8533292 7181645 668984 91% 430272 539806 44% /cache2
/dev/da2d 8533292 7198600 652029 92% 434726 535352 45% /cache3
/dev/da3d 8533292 7208948 641681 92% 427866 542212 44% /cache4
如果i节点的使用(%iused)少于空间使用(Capacity),那就很好。不幸的是,你不能对已经存在的文件系统增加更多i节点。假如你发现运行超出了i节点,那就必须停止,并且重新创建文件系统。假如你不愿意这样做,那么请削减cache_dir的大小。
7.1.3.2 在磁盘空间和进程大小之间的联系
Squid的磁盘空间使用也直接影响了它的内存使用。每个在磁盘中存在的对象,要求少量的内存。使用内存来索引磁盘数据。假如你增加了新的cache目录,或者增加了磁盘cache大小,请确认你已有足够的自由内存。假如的进程大小达到或超过了系统的物理内存容量,的性能下降得非常块。
Squid的cache目录里的每个对象消耗76或112字节的内存,这依赖于你的系统。内存以StoreEntry, MD5 Digest, 和LRU policy node结构来分配。小指令(例如,32位)系统,象那些基于Intel Pentium的,取76字节。使用64位指令CPU的系统,每个目标取112字节。通过阅读cache管理的内存管理文档,你能发现这些结构在你的系统中耗费多少内存(请见14.2.1.2章)。
不幸的是,难以精确预测对于给定数量的磁盘空间,需要使用多少附加内存。它依赖于实际响应大小,而这个大小基于时间波动。另外,Squid还为其他数据结构和目的分配内存。不要假设你的估计正确。你该经常监视的进程大小,假如必要,考虑削减cache大小。
7.1.4 参数:L1和L2
对ufs,aufs,和diskd机制,在cache目录下创建二级目录树。L1和L2参数指定了第一级和第二级目录的数量。默认的是16和256。图7-1显示文件系统结构。
Figure 7-1. 基于ufs存储机制的cache目录结构
(略图)
某些人认为依赖于L1和L2的特殊值,会执行得更好或更差。这点听起来有关系,即小目录比大目录被检索得更快。这样,L1和L2也许该足够大,以便L2目录的文件更少。
例如,假设你的cache目录存储了7000M,假设实际文件大小是10KB,你能在这个cache_dir里存储700,000个文件。使用16个L1和256个L2目录,总共有4096个二级目录。700,000/4096的结果是,每个二级目录大约有170个文件。
如果L1和L2的值比较小,那么使用 -z创建交换目录的过程,会执行更快。这样,假如你的cache文件确实小,你也许该减少L1和L2目录的数量。
Squid给每个cache目标分配一个唯一的文件号。这是个32位的整数,它唯一标明磁盘中的文件。使用相对简单的算法,将文件号转换位路径名。该算法使用L1和L2作为参数。这样,假如你改变了L1和L2,你改变了从文件号到路径名的映射关系。对非空的cache_dir改变这些参数,导致存在的文件不可访问。在cache目录激活后,你永不要改变L1和L2值。
Squid在cache目录顺序中分配文件号。文件号到路径名的算法(例如,storeUfsDirFullPath( )),用以将每组L2文件映射到同样的二级目录。Squid使用了参考位置来做到这点。该算法让HTML文件和它内嵌的图片更可能的保存在同一个二级目录中。某些人希望均匀的将cache文件放在每个二级目录中。然而,当cache初始写入时,你可以发现仅仅开头的少数目录包含了一些文件,例如:
% cd /cache0; du -k
2164 ./00/00
2146 ./00/01
2689 ./00/02
1974 ./00/03
2201 ./00/04
2463 ./00/05
2724 ./00/06
3174 ./00/07
1144 ./00/08
1 ./00/09
1 ./00/0A
1 ./00/0B
这是完全正常的,不必担心。
7.1.5 参数:Options
Squid有2个依赖于不同存储机制的cache_dir选项:read-only标签和max-size值。
7.1.5.1 read-only
read-only选项指示Squid继续从cache_dir读取文件,但不往里面写新目标。它在.conf文件里看起来如下:
cache_dir ufs /cache0 7000 16 256 read-only
假如你想把cache文件从一个磁盘迁移到另一个磁盘,那么可使用该选项。如果你简单的增加一个cache_dir,并且删除另一个,的命中率会显著下降。在旧目录是read-only时,你仍能从那里获取cache命中。在一段时间后,就可以从配置文件里删除read-only缓存目录。
7.1.5.2 max-size
使用该选项,你可以指定存储在cache目录里的最大目标大小。例如:
cache_dir ufs /cache0 7000 16 256 max-size=1048576
注意值是以字节为单位的。在大多数情况下,你不必增加该选项。假如你做了,请尽力将所有cache_dir行以max-size大小顺序来存放(从小到大)。
7.2 磁盘空间基准
cache_swap_low和cache_swap_high指令控制了存储在磁盘上的对象的置换。它们的值是最大cache体积的百分比,这个最大cache体积来自于所有cache_dir大小的总和。例如:
cache_swap_low 90
cache_swap_high 95
如果总共磁盘使用低于cache_swap_low,不会删除cache目标。如果cache体积增加,会逐渐删除目标。在稳定状态下,你发现磁盘使用总是相对接近cache_swap_low值。你可以通过请求cache管理器的storedir页面来查看当前磁盘使用状况(见14.2.1.39章)。
请注意,改变cache_swap_high也许不会对的磁盘使用有太大效果。在的早期版本里,该参数有重要作用;然而现在,它不是这样了。
7.3 对象大小限制
你可以控制缓存对象的最大和最小体积。比maximum_object_size更大的响应不会被缓存在磁盘。然而,它们仍然是代理方式的。在该指令后的逻辑是,你不想某个非常大的响应来浪费空间,这些空间能被许多小响应更好的利用。该语法如下:
maximum_object_size size-specification
如下是一些示例:
maximum_object_size 100 KB
maximum_object_size 1 MB
maximum_object_size 12382 bytes
maximum_object_size 2 GB
Squid以两个不同的方法来检查响应大小。假如响应包含了Content-Length头部,将这个值与maximum_object_size值进行比较。假如前者大于后者,该对象立刻不可缓存,并且不会消耗任何磁盘空间。
不幸的是,并非每个响应都有Content-Length头部。在这样的情形下,将响应写往磁盘,把它当作来自原始服务器的数据。在响应完成后,再检查对象大小。这样,假如对象的大小达到 maximum_object_size限制,它继续消耗磁盘空间。仅仅当在做读取响应的动作时,总共cache大小才会增大。
换句话说,活动的,或者传输中的目标,不会对内在的cache大小值有影响。这点有好处,因为它意味着不会删除cache里的其他目标,除非目标不可缓存,并对总共cache大小有影响。然而,这点也有坏处,假如响应非常大,可能运行超出了磁盘自由空间。为了减少发生这种情况的机会,你应该使用reply_body_max_size指令。某个达到reply_body_max_size限制的响应立即被删除。
Squid也有一个minimum_object_size指令。它允许你对缓存对象的大小设置最低限制。比这个值更小的响应不会被缓存在磁盘或内存里。注意这个大小是与响应的内容长度(例如,响应body大小)进行比较,后者包含在HTTP头部里。
7.4 分配对象到缓存目录
当想将某个可缓存的响应存储到磁盘时,它调用一个函数,用以选择cache目录。然后它在选择的目录里打开一个磁盘文件用于写。假如因为某些理由,open()调用失败,响应不会被存储。在这样的情况下,不会试图在其他cache目录里打开另一个磁盘文件。
Squid有2个cache_dir选择算法。默认的算法叫做lease-load;替代的算法是round-robin。
least-load算法,就如其名字的意义一样,它选择当前工作负载最小的cache目录。负载概念依赖于存储机制。对aufs,coss和diskd机制来说,负载与挂起操作的数量有关。对ufs来说,负载是不变的。在cache_dir负载相等的情况下,该算法使用自由空间和最大目标大小作为附加选择条件。
该选择算法也取决于max-size和read-only选项。假如知道目标大小超出了限制,它会跳过这个cache目录。它也会跳过任何只读目录。
round-robin算法也使用负载作为衡量标准。它选择某个负载小于100%的cache目录,当然,该目录里的存储目标没有超出大小限制,并且不是只读的。
在某些情况下,可能选择cache目录失败。假如所有的cache_dir是满负载,或者所有目录的实际目标大小超出了max-size限制,那么这种情况可能发生。这时,不会将目标写往磁盘。你可以使用cache管理器来跟踪选择cache目录失败的次数。请见store_io页(14.2.1.41章),找到create.select_fail行。
7.5 置换策略
cache_replacement_policy指令控制了的磁盘cache的置换策略。Squid2.5版本提供了三种不同的置换策略:最少近来使用(LRU),贪婪对偶大小次数(GDSF),和动态衰老最少经常使用(LFUDA)。
LRU是默认的策略,并非对,对其他大部分cache产品都是这样。LRU是流行的选择,因为它容易执行,并提供了非常好的性能。在32位系统上,LRU相对于其他使用更少的内存(每目标12对16字节)。在64位系统上,所有的策略每目标使用24字节。
在过去,许多研究者已经提议选择LRU。其他策略典型的被设计来改善cache的其他特性,例如响应时间,命中率,或字节命中率。然而研究者的改进结果也可能在误导人。某些研究使用并不现实的小cache目标;其他研究显示当cache大小增加时,置换策略的选择变得不那么重要。
假如你想使用GDSF或LFUDA策略,你必须在./configure时使用--enable-removal-policies选项(见3.4.1章)。Martin Arlitt和HP实验室的John Dilley为写了GDSF和LFUDA算法。你可以在线阅读他们的文档:
我在O'Reilly出版的书"Web Caching",也讨论了这些算法。
cache_replacement_policy指令的值是唯一的,这点很重要。不象.conf里的大部分其他指令,这个指令的位置很重要。cache_replacement_policy指令的值在解析cache_dir指令时,被实际用到。通过预先设置替换策略,你可以改变cache_dir的替换策略。例如:
cache_replacement_policy lru
cache_dir ufs /cache0 2000 16 32
cache_dir ufs /cache1 2000 16 32
cache_replacement_policy heap GDSF
cache_dir ufs /cache2 2000 16 32
cache_dir ufs /cache3 2000 16 32
在该情形中,头2个cache目录使用LRU置换策略,接下来2个cache目录使用GDSF。请记住,假如你已决定使用cache管理器的config选项(见14.2.1.7章),这个置换策略指令的特性就非常重要。cache管理器仅仅输出最后一个置换策略的值,将它置于所有的cache目录之前。例如,你可能在.conf里有如下行:
cache_replacement_policy heap GDSF
cache_dir ufs /tmp/cache1 10 4 4
cache_replacement_policy lru
cache_dir ufs /tmp/cache2 10 4 4
但当你从cache管理器选择config时,你得到:
cache_replacement_policy lru
cache_dir ufs /tmp/cache1 10 4 4
cache_dir ufs /tmp/cache2 10 4 4
就象你看到的一样,对头2个cache目录的heap GDSF设置被丢失了。
7.6 删除缓存对象
在某些情况下,你必须从的cache里手工删除一个或多个对象。这些情况可能包括:
+ 你的用户抱怨总接收到过时的数据;
+ 你的cache因为某个响应而“中毒”;
+ Squid的cache索引在经历磁盘I/O错误或频繁的crash和重启后,变得有问题;
+ 你想删除一些大目标来释放空间给新的数据;
+ Squid总从本地服务器中cache响应,现在你不想它这样做。
上述问题中的一些可以通过强迫web浏览器reload来解决。然而,这并非总是可靠。例如,一些浏览器通过载入另外的程序,从而显示某些类容类型;那个程序可能没有reload按钮,或甚至它了解cache的情况。
假如必要,你总可以使用client程序来reload缓存目标。简单的在uri前面使用-r选项:
% client -r
假如你碰巧在refresh_pattern指令里设置了ignore-reload选项,你和你的用户将不能强迫缓存响应更新。在这样的情形下,你最好清除这些有错误的缓存对象。
7.6.1 删除个别对象
Squid接受一种客户请求方式,用于删除cache对象。PURGE方式并非官方HTTP请求方式之一。它与Delete不同,对后者,将其转发到原始服务器。PURGE请求要求删除在uri里提交的目标。返回200(OK)或404(Not Found)。
PURGE方式某种程度上有点危险,因为它删除了cache目标。除非你定义了相应的ACL,否则禁止PURGE方式。正常的,你仅仅允许来自本机和少数可信任主机的PURGE请求。配置看起来如下:
acl AdminBoxes src 127.0.0.1 172.16.0.1 192.168.0.1
acl Purge method PURGE
http_access allow AdminBoxes Purge
http_access deny Purge
client程序提供了产生PURGE请求的容易方法,如下:
% client -m PURGE
代替的,你可以使用其他工具(例如perl脚本)来产生你自己的HTTP请求。它非常简单:
PURGE HTTP/1.0
Accept: */*
注意某个单独的URI不唯一标明一个缓存响应。Squid也在cache关键字里使用原始请求方式。假如响应包含了不同的头部,它也可以使用其他请求头。当你发布PURGE请求时,Squid使用GET和HEAD的原始请求方式来查找缓存目标。而且,Squid会删除响应里的所有variants,除非你在PURGE请求的相应头部里指定了要删除的variants。Squid仅仅删除GET和HEAD请求的variants。
7.6.2 删除一组对象
不幸的是,Squid没有提供一个好的机制,用以立刻删除一组对象。这种要求通常出现在某人想删除所有属于同一台原始服务器的对象时。
因为很多理由,不提供这种功能。首先,必须遍历所有缓存对象,执行线性搜索,这很耗费CPU,并且耗时较长。当在搜索时,用户会面临性能下降问题。第二,在内存里对URI保持MD5算法,MD5是单向哈希,这意味着,例如,你不能确认是否某个给定的MD5哈希是由包含""字符串的URI产生而来。唯一的方法是从原始URI重新计算MD5值,并且看它们是否匹配。因为没有保持原始的URI,它不能执行这个重计算。
那么该怎么办呢?
你可以使用access.log里的数据来获取URI列表,它们可能位于cache里。然后,将它们用于client或其他工具来产生PURGE请求,例如:
% awk '{print $7}' /usr/local//var/logs/access.log \
| grep \
| xargs -n 1 client -m PURGE
7.6.3 删除所有对象
在极度情形下,你可能需要删除整个cache,或至少某个cache目录。首先,你必须确认没有在运行。
让忘记所有缓存对象的最容易的方法之一,是覆盖swap.state文件。注意你不能简单的删除swap.state文件,因为接着要扫描cache目录和打开所有的目标文件。你也不能简单的截断swap.state为0大小。代替的,你该放置一个单字节在里面,例如:
# echo '' > /usr/local//var/cache/swap.state
当读取swap.state文件时,它获取到了错误,因为在这里的记录太短了。下一行读取就到了文件结尾,完成重建过程,没有装载任何目标元数据。
注意该技术不会从磁盘里删除cache文件。你仅仅使认为它的cache是空的。当运行时,它增加新文件到cache里,并且可能覆盖旧文件。在某些情形下,这可能导致你的磁盘使用超出了自由空间。假如这样的事发生,你必须在再次重启前删除旧文件。
删除cache文件的方法之一是使用rm。然而,它通常花费很长的时间来删除所有被创建的文件。为了让快速启动,你可以重命名旧cache目录,创建一个新目录,启动,然后同时删除旧目录。例如:
# -k shutdown
# cd /usr/local//var
# mv cache oldcache
# mkdir cache
# chown nobody:nobody cache
# -z
# -s
# rm -rf oldcache &
另一种技术是简单的在cache文件系统上运行newfs(或mkfs)。这点仅在你的cache_dir使用整个磁盘分区时才可以运行。
7.7 refresh_pattern
refresh_pattern指令间接的控制磁盘缓存。它帮助决定,是否某个给定请求是cache命中,或作为cache丢失对待。宽松的设置增加了你的cache命中率,但也增加了用户接收过时响应的机会。另一方面,保守的设置,降低了cache命中率和过时响应。
refresh_pattern规则仅仅应用到没有明确过时期限的响应。原始服务器能使用Expires头部,或者Cache-Control:max-age指令来指定过时期限。
你可以在配置文件里放置任意数量的refresh_pattern行。按顺序查找它们以匹配正则表达式。当找到一个匹配时,它使用相应的值来决定,某个缓存响应是存活还是过期。refresh_pattern语法如下:
refresh_pattern [-i] regexp min percent max [options]
例如:
refresh_pattern -i \.jpg$ 30 50% 4320 reload-into-ims
refresh_pattern -i \.png$ 30 50% 4320 reload-into-ims
refresh_pattern -i \.htm$ 0 20% 1440
refresh_pattern -i \.html$ 0 20% 1440
refresh_pattern -i . 5 25% 2880
regexp参数是大小写敏感的正则表达式。你可以使用-i选项来使它们大小写不敏感。按顺序来检查refresh_pattern行;当正则表达式之一匹配URI时,它停止搜索。
min参数是分钟数量。它是过时响应的最低时间限制。如果某个响应驻留在cache里的时间没有超过这个最低限制,那么它不会过期。类似的,max参数是存活响应的最高时间限制。如果某个响应驻留在cache里的时间高于这个最高限制,那么它必须被刷新。
在最低和最高时间限制之间的响应,会面对的最后修改系数 (LM-factor)算法。对这样的响应,计算响应的年龄和最后修改系数,然后将它作为百分比值进行比较。响应年龄简单的就是从原始服务器产生,或最后一次验证响应后,经历的时间数量。源年龄在Last-Modified和Date头部之间是不同的。LM-factor是响应年龄与源年龄的比率。
图7-2论证了LM-factor算法。缓存了某个目标3个小时(基于Date和Last-Modified头部)。LM-factor的值是50%,响应在接下来的1.5个小时里是存活的,在这之后,目标会过期并被当作过时处理。假如用户在存活期间请求cache目标,返回没有确认的cache命中。若在过时期间发生请求,转发确认请求到原始服务器。
图7-2 基于LM-factor计算过期时间
(略图)
理解检查不同值的顺序非常重要。如下是的refresh_pattern算法的简单描述:
+ 假如响应年龄超过refresh_pattern的max值,该响应过期;
+ 假如LM-factor少于refresh_pattern百分比值,该响应存活;
+ 假如响应年龄少于refresh_pattern的min值,该响应存活;
+ 其他情况下,响应过期。
refresh_pattern指令也有少数选项导致违背HTTP协议规范。它们如下:
override-expire
该选项导致在检查Expires头部之前,先检查min值。这样,一个非零的min时间让返回一个未确认的cache命中,即使该响应准备过期。
override-lastmod
改选项导致在检查LM-factor百分比之前先检查min值。
reload-into-ims
该选项让在确认请求里,以no-cache指令传送一个请求。换句话说,在转发请求之前,对该请求增加一个If-Modified-Since头部。注意这点仅仅在目标有Last-Modified时间戳时才能工作。外面进来的请求保留no-cache指令,以便它到达原始服务器。
ignore-reload
该选项导致忽略请求里的任何no-cache指令。
第8章 高级磁盘缓存主题
8.1 是否存在磁盘I/O瓶颈?
Web缓存器例如,通常在磁盘I/O变成瓶颈时,不会正确的体现和告知你。代替的是,随着负载的增加,响应时间和/或命中率会更低效。当然,响应时间和命中率可能因为其他原因而改变,例如网络延时和客户请求方式的改变。
也许探测cache性能瓶颈的最好方式是做压力测试,例如Web Polygraph。压力测试的前提是你能完全控制环境,消除未知因素。你可以用不同的cache配置来重复相同的测试。不幸的是,压力测试通常需要大量的时间,并要求有空闲的系统(也许它们正在使用中)。
假如你有资源执行压力测试,请以标准的cache工作负载开始。当你增加负载时,在某些点上你能看到明显的响应延时和/或命中率下降。一旦你观 察到这样的性能降低,就禁止掉磁盘缓存,再测试一次。你可以配置从来不缓存任何响应(使用null存储机制,见8.7章)。代替的,你能配置工 作负载到100%不可cache响应。假如不使用cache时,平均响应时间明显更好,那么可以确认磁盘I/O是该水平吞吐量的瓶颈。
假如你没有时间或没有资源来执行压力测试,那么可检查的运行时统计来查找磁盘I/O瓶颈。cache管理器的General Runtime Information 页面(见14章)会显示出cache命中和cache丢失的中值响应时间。
Median Service Times (seconds) 5 min 60 min:
HTTP Requests (All): 0.39928 0.35832
Cache Misses: 0.42149 0.39928
Cache Hits: 0.12783 0.11465
Near Hits: 0.37825 0.39928
Not-Modified Replies: 0.07825 0.07409
对健壮的缓存来说,命中显然快于丢失。中值命中响应时间典型的少于0.5秒或更少。我强烈建议你使用SNMP或其他的网络监视工具来从缓存采集定期测量值。如果平均命中响应时间增加得太明显,意味着系统有磁盘I/0瓶颈。
假如你认为产品cache面临此类问题,可以用前面提到的同样的技术来验证你的推测。配置不cache任何响应,这样就避开了所有磁盘I/O。然后仔细观察cache丢失响应时间。假如它降下去,那么你的推测该是正确的。
一旦你确认了磁盘吞吐能力是的性能瓶颈,那么可做许多事来改进它。其中一些方法要求重编译,然而另一些相对较简单,只需调整Unix文件系统。
8.2 文件系统调整选项
首先,从来不在的缓存目录中使用RAID。以我的经验看,RAID总是降低使用的文件系统的性能。最好有许多独立的文件系统,每个文件系统使用单独的磁盘驱动器。
我发现4个简单的方法来改进的UFS性能。其中某些特指某种类型的操作系统例如BSD和Linux,也许对你的平台不太合适:
1.某些UFS支持一个noatime的mount选项。使用noatime选项来mount的文件系统,不会在读取时,更新相应的i节点访问时间。使用该选项的最容易的方法是在/etc/fstab里增加如下行:
# Device Mountpoint FStype Options Dump Pass#
/dev/ad1s1c /cache0 ufs rw,noatime 0 0
2.检查mount(的manpage 里的async选项。设置了该选项,特定的I/O操作(例如更新目录)会异步执行。某些系统的文档会标明这是个危险的标签。某天你的系统崩溃,你也许会丢 失整个文件系统。对许多安装来说,执行性能的提高值得冒此风险。假如你不介意丢失整个cache内容,那么可以使用该选项。假如cache数据 非常有价值,async选项也许不适合你。
3.BSD有一个功能叫做软更新。软更新是BSD用于Journaling文件系统的代替品。在FreeBSD上,你可以在没有mount的文件系统中,使用tunefs命令来激活该选项:
# umount /cache0
# tunefs -n enable /cache0
# mount /cache0
4.你对每个文件系统运行一次tunefs命令就可以了。在系统重启时,软更新自动在文件系统中激活了。
在OpenBSD和NetBSD中,可使用softdep mount选项:
# Device Mountpoint FStype Options Dump Pass#
/dev/sd0f /usr ffs rw,softdep 1 2
假如你象我一样,你可能想知道在async选项和软更新选项之间有何不同。一个重要的区别是,软更新代码被设计成在系统崩溃事件中,保持文件系统的一致性,而async选项不是这样的。这也许让你推断async执行性能好于软更新。然而,如我在附录D中指出的,事实相反。
以前我提到过,UFS性能特别是写性能,依赖于空闲磁盘的数量。对空文件系统的磁盘写操作,要比满文件系统快得多。这是UFS的最小自由空间参 数,和空间/时间优化权衡参数背后的理由之一。假如cache磁盘满了,执行性能看起来很糟,那么试着减少cache_dir的容量值,以便更 多的自由空间可用。当然,减少cache大小也会降低命中率,但响应时间的改进也许值得这么做。假如你给缓存配置新的设备,请考虑使用超过你需 要的更大磁盘,并且仅仅使用空间的一半。
8.3 可选择的文件系统
某些操作系统支持不同于UFS(或ext2fs)的文件系统。Journaling文件系统是较普遍的选择。在UFS和Journaling文件 系统之间的主要不同在于它们处理更新的方式。在UFS下,更新是实时的。例如,当你改变了某个文件并且将它存储到磁盘,新数据就替换了旧数据。当你删除文 件时,UFS直接更新了目录。
Journaling文件系统与之相反,它将更新写往独立的记帐系统,或日志文件。典型的你能选择是否记录文件改变或元数据改变,或两者兼备。某 个后台进程在空闲时刻读取记帐,并且执行实际的改变操作。Journaling文件系统典型的在系统崩溃后比UFS恢复更快。在系统崩溃后, Journaling文件系统简单的读取记帐,并且提交所有显著的改变。
Journaling文件系统的主要弊端在于它们需要额外的磁盘写操作。改变首先写往日志文件,然后才写往实际的文件或目录。这对web缓存影响尤其明显,因为首先web缓存倾向于更多的磁盘写操作。
Journaling文件系统对许多操作系统可用。在Linux上,你能选择ext3fs,reiserfs, XFS,和其他的。XFS也可用在SGI/IRIX,它原始是在这里开发的。Solaris用户能使用Veritas文件系统产品。TRU64(以前的 Digital Unix)高级文件系统(advfs)支持Journaling。
你可以不改变的任何配置而使用Journaling文件系统。简单的创建和挂载在操作系统文档里描述的文件系统,而不必改变.cf配置文件里的cache_dir行。
用类似如下命令在Linux中制作reiserfs文件系统:
# /sbin/mkreiserfs /dev/sda2
对XFS,使用:
# mkfs -t xfs -f /dev/sda2
注意ext3fs其实简单的就是激活了记帐的ext2fs。当创建该文件系统时,对mke2fs使用-j选项:
# /sbin/mke2fs -j /dev/sda2
请参考其他操作系统的相关文档。
8.4 aufs存储机制
aufs存储机制已经发展到超出了改进磁盘I/O响应时间的最初尝试。"a"代表着异步I/O。默认的ufs和aufs之间的唯一区别,在于I/O是否被主进程执行。数据格式都是一样的,所以你能在两者之间轻松选择,而不用丢失任何cache数据。
aufs使用大量线程进行磁盘I/O操作。每次需要读写,打开关闭,或删除cache文件时,I/O请求被分派到这些线程之一。当线程 完成了I/O后,它给主进程发送信号,并且返回一个状态码。实际上在2.5中,某些文件操作默认不是异步执行的。最明显的,磁盘写总 是同步执行。你可以修改src/fs/aufs/store_asyncufs.h文件,将ASYNC_WRITE设为1,并且重编译。
aufs代码需要pthreads库。这是POSIX定义的标准线程接口。尽管许多Unix系统支持pthreads库,但我经常遇到兼容性问 题。aufs存储系统看起来仅仅在Linux和Solaris上运行良好。在其他操作系统上,尽管代码能编译,但也许会面临严重的问题。
为了使用aufs,可以在./configure时增加一个选项:
% ./configure --enable-storeio=aufs,ufs
严格讲,你不必在storeio模块列表中指定ufs。然而,假如你以后不喜欢aufs,那么就需要指定ufs,以便能重新使用稳定的ufs存储机制。
假如愿意,你也能使用—with-aio-threads=N选项。假如你忽略它,基于aufs cache_dir的数量,自动计算可使用的线程数量。表8-1显示了1-6个cache目录的默认线程数量。
Table 8-1. Default number of threads for up to six cache directories
cache_dirs Threads
1 16
2 26
3 32
4 36
5 40
6 44
将aufs支持编译进后,你能在.conf文件里的cache_dir行后指定它:
cache_dir aufs /cache0 4096 16 256
在激活了aufs并启动后,请确认每件事仍能工作正常。可以运行tail -f store.log一会儿,以确认缓存目标被交换到磁盘。也可以运行tail -f cache.log并且观察任何新的错误或警告。
8.4.1 aufs如何工作
Squid通过调用pthread_create()来创建大量的线程。所有线程在任何磁盘活动之上创建。这样,即使空闲,你也能见到所有的线程。
无论何时,想执行某些磁盘I/O操作(例如打开文件读),它分配一对数据结构,并将I/O请求放进队列中。线程循环读取队列,取得I/O请求并执行它们。因为请求队列共享给所有线程,使用独享锁来保证仅仅一个线程能在给定时间内更新队列。
I/O操作阻塞线程直到它们被完成。然后,将操作状态放进一个完成队列里。作为完整的操作,主进程周期性的检查完成队列。请求磁盘I/O的模块被通知操作已完成,并获取结果。
你可能已猜想到,aufs在多CPU系统上优势更明显。唯一的锁操作发生在请求和结果队列。然而,所有其他的函数执行都是独立的。当主进程在一个CPU上执行时,其他的CPU处理实际的I/O系统调用。
8.4.2 aufs发行
线程的有趣特性是所有线程共享相同的资源,包括内存和文件描述符。例如,某个线程打开一个文件,文件描述符为27,所有其他线程能以相同的文件描 述符来访问该文件。可能你已知道,在初次管理时,文件描述符短缺是较普遍问题。Unix内核典型的有两种文件描述符限制:
进程级的限制和系统级的限制。你也许认为每个进程拥有256个文件描述符足够了(因为使用线程),然而并非如此。在这样的情况下,所有线程共享少量的文件描述符。请确认增加系统的进程文件描述符限制到4096或更高,特别在使用aufs时。
调整线程数量有点棘手。在某些情况下,可在cache.log里见到如下警告:
2003/09/29 13:42:47| aio_queue_request: WARNING - Disk I/O overloading
这意味着有大量的I/O操作请求充满队列,等待着可用的线程。你首先会想到增加线程数量,然而我建议,你该减少线程数量。
增加线程数量也会增加队列的大小。超过一定数量,它不会改进aufs的负载能力。它仅仅意味着更多的操作变成队列。太长的队列导致响应时间变长,这绝不是你想要的。
减少线程数量和队列大小,意味着检测负载条件更快。当某个cache_dir超载,它会从选择算法里移除掉(见7.4章)。然后, 选择其他的cache_dir或简单的不存储响应到磁盘。这可能是较好的解决方法。尽管命中率下降,响应时间却保持相对较低。
8.4.3 监视aufs操作
Cache管理器菜单里的Async IO Counters选项,可以显示涉及到aufs的统计信息。它显示打开,关闭,读写,stat,和删除接受到的请求的数量。例如:
% client mgr:aio_counts
...
ASYNC IO Counters:
Operation # Requests
open 15318822
close 15318813
cancel 15318813
write 0
read 19237139
stat 0
unlink 2484325
check_callback 311678364
queue 0
取消(cancel)计数器正常情况下等同于关闭(close)计数器。这是因为close函数总是调用cancel函数,以确认任何未决的I/O操作被忽略。
写(write)计数器为0,因为该版本的执行同步写操作,即使是aufs。
check_callbak计数器显示主进程对完成队列检查了多少次。
queue值显示当前请求队列的长度。正常情况下,队列长度少于线程数量的5倍。假如你持续观察到队列长度大于这个值,说明配得有问题。增加更多的线程也许有帮助,但仅仅在特定范围内。
8.5 diskd存储机制
diskd(disk守护进程的短称)类似于aufs,磁盘I/O被外部进程来执行。不同于aufs的是,diskd不使用线程。代替的,它通过消息队列和共享内存来实现内部进程间通信。
消息队列是现代Unix操作系统的标准功能。许多年以前在AT&T的Unix System V的版本1上实现了它们。进程间的队列消息以较少的字节传递:32-40字节。每个diskd进程使用一个队列来接受来自的请求,并使用另一个队列来传回请求。
8.5.1 diskd如何工作
Squid对每个cache_dir创建一个diskd进程。这不同于aufs,aufs对所有的cache_dir使用一个大的线程池。对每个 I/O操作,发送消息到相应的diskd进程。当该操作完成后,diskd进程返回一个状态消息给。和diskd进程维护 队列里的消息的顺序。这样,不必担心I/O会无序执行。
对读和写操作,和diskd进程使用共享内存区域。两个进程能对同一内存区域进行读和写。例如,当产生读请求时,它告诉 diskd进程在内存中何处放置数据。diskd将内存位置传递给read()系统调用,并且通过发送队列消息,通知该过程完成了。然后 从共享内存区域访问最近的可读数据。
diskd与aufs本质上都支持的无阻塞磁盘I/O。当diskd进程在I/O操作上阻塞时,有空去处理其他任务。在 diskd进程能跟上负载情况下,这点确实工作良好。因为主进程现在能够去做更多工作,当然它有可能会加大diskd的负载。diskd有两个 功能来帮助解决这个问题。
首先,等待diskd进程捕获是否队列超出了某种极限。默认值是64个排队消息。假如diskd进程获取的数值远大于此, 会休眠片刻,并等待diskd完成一些未决操作。这本质上让进入阻塞I/O模式。它也让更多的CPU时间对diskd进程可用。通过指定 cache_dir行的Q2参数的值,你可以配置这个极限值:
cache_dir diskd /cache0 7000 16 256 Q2=50
第二,假如排队操作的数量抵达了另一个极限,会停止要求diskd进程打开文件。这里的默认值是72个消息。假如想打开一个 磁盘文件读或写,但选中的cache_dir有太多的未完成操作,那么打开请求会失败。当打开文件读时,会导致cache丢失。当打开文件写时,会阻碍 存储cache响应。这两种情况下用户仍能接受到有效响应。唯一实际的影响是的命中率下降。这个极限用Q1参数来配置:
cache_dir diskd /cache0 7000 16 256 Q1=60 Q2=50
注意在某些版本的中,Q1和Q2参数混杂在默认的配置文件里。最佳选择是,Q1应该大于Q2。
8.5.2 编译和配置diskd
为了使用diskd,你必须在运行./configure时,在--enable-storeio列表后增加一项:
% ./configure --enable-storeio=ufs,diskd
diskd看起来是可移植的,既然共享内存和消息队列在现代Unix系统上被广泛支持。然而,你可能需要调整与这两者相关的内核限制。内核典型的有如下可用参数:
MSGMNB
每个消息队列的最大字节限制。对diskd的实际限制是每个队列大约100个排队消息。传送的消息是32-40字节,依赖于你的CPU体系。这样,MSGMNB应该是4000或更多。为安全起见,我推荐设置到8192。
MSGMNI
整个系统的最大数量的消息队列。对每个cache_dir使用两个队列。假如你有10个磁盘,那就有20个队列。你也许该增加更多,因为其他应用程序也要使用消息队列。我推荐的值是40。
MSGGSZ
消息片断的大小(字节)。大于该值的消息被分割成多个片断。我通常将这个值设为64,以使diskd消息不被分割成多个片断。
MSGSEG
在单个队列里能存在的最大数量的消息片断。正常情况下,限制队列的长度为100个排队消息。记住,在64位系统中,假如你没有增加MSGSSZ的值到64,那么每个消息就会被分割成不止1个片断。为了安全起见,我推荐设置该值到512。
MSGTQL
整个系统的最大数量的消息。至少是cache_dir数量的100倍。在10个cache目录情况下,我推荐设置到2048。
MSGMAX
单个消息的最大size。对Squid来说,64字节足够了。然而,你系统中的其他应用程序可能要用到更大的消息。在某些操作系统例如BSD中, 你不必设置这个。BSD自动设置它为MSGSSZ * MSGSEG。其他操作系统中,你也许需要改变这个参数的默认值,你可以设置它与MSGMNB相同。
SHMSEG
每个进程的最大数量的共享内存片断。对每个cache_dir使用1个共享内存标签。我推荐设置到16或更高。
SHMMNI
共享内存片断数量的系统级的限制。大多数情况下,值为40足够了。
SHMMAX
单个共享内存片断的最大size。默认的,对每个片断使用大约409600字节。
为安全起见,我推荐设置到2MB,或2097152。
SHMALL
可分配的共享内存数量的系统级限制。在某些系统上,SHMALL可能表示成页数量,而不是字节数量。在10个cache_dir的系统上,设置该值到16MB(4096页)足够了,并有足够的保留给其他应用程序。
在BSD上配置消息队列,增加下列选项到内核配置文件里:
# System V message queues and tunable parameters
options SYSVMSG # include support for message queues
options MSGMNB=8192 # max characters per message queue
options MSGMNI=40 # max number of message queue identifiers
options MSGSEG=512 # max number of message segments per queue
options MSGSSZ=64 # size of a message segment MUST be power of 2
options MSGTQL=2048 # max number of messages in the system
options SYSVSHM
options SHMSEG=16 # max shared mem segments per process
options SHMMNI=32 # max shared mem segments in the system
options SHMMAX=2097152 # max size of a shared mem segment
options SHMALL=4096 # max size of all shared memory (pages)
在Linux上配置消息队列,增加下列行到/etc/sysctl.conf:
kernel.msgmnb=8192
kernel.msgmni=40
kernel.msgmax=8192
kernel.shmall=2097152
kernel.shmmni=32
kernel.shmmax=16777216
另外,假如你需要更多的控制,可以手工编辑内核资源文件中的include/linux/msg.h和include/linux/shm.h。
在Solaris上,增加下列行到/etc/system,然后重启:
set msgsys:msginfo_msgmax=8192
set msgsys:msginfo_msgmnb=8192
set msgsys:msginfo_msgmni=40
set msgsys:msginfo_msgssz=64
set msgsys:msginfo_msgtql=2048
set shmsys:shminfo_shmmax=2097152
set shmsys:shminfo_shmmni=32
set shmsys:shminfo_shmseg=16
在Digital Unix(TRU64)上,可以增加相应行到BSD风格的内核配置文件中,见前面所叙。另外,你可使用sysconfig命令。首先,创建如下的ipc.stanza文件:
ipc:
msg-max = 2048
msg-mni = 40
msg-tql = 2048
msg-mnb = 8192
shm-seg = 16
shm-mni = 32
shm-max = 2097152
shm-max = 4096
然后,运行这个命令并重启:
# sysconfigdb -a -f ipc.stanza
一旦你在操作系统中配置了消息队列和共享内存,就可以在.conf里增加如下的cache_dir行:
cache_dir diskd /cache0 7000 16 256 Q1=72 Q2=64
cache_dir diskd /cache1 7000 16 256 Q1=72 Q2=64
...
8.5.3 监视diskd
监视diskd运行的最好方法是使用cache管理器。请求diskd页面,例如:
% client mgr:diskd
...
sent_count: 755627
recv_count: 755627
max_away: 14
max_shmuse: 14
open_fail_queue_len: 0
block_queue_len: 0
OPS SUCCESS FAIL
open 51534 51530 4
create 67232 67232 0
close 118762 118762 0
unlink 56527 56526 1
read 98157 98153 0
write 363415 363415 0
请见14.2.1.6章关于该输出的详细描述。
8.6 coss存储机制
循环目标存储机制(Cyclic Object Storage Scheme,coss)尝试为定制一个新的文件系统。在ufs基础的机制下,主要的性能瓶颈来自频繁的open()和unlink()系统调 用。因为每个cache响应都存储在独立的磁盘文件里,总是在打开,关闭,和删除文件。
与之相反的是,coss使用1个大文件来存储所有响应。在这种情形下,它是特定供使用的,小的定制文件系统。coss实现许多底层文件系统的正常功能,例如给新数据分配空间,记忆何处有自由空间等。
不幸的是,coss仍没开发完善。coss的开发在过去数年里进展缓慢。虽然如此,基于有人喜欢冒险的事实,我还是在这里描述它。
8.6.1 coss如何工作
在磁盘上,每个coss cache_dir是一个大文件。文件大小一直增加,直到抵达它的大小上限。这样,从文件的开头处开始,覆盖掉任何存储在这里的数据。然后,新的目标总是存储在该文件的末尾处。
实际上并不立刻写新的目标数据到磁盘上。代替的,数据被拷贝进1MB的内存缓冲区,叫做stripe。在stripe变满后,它被写往磁盘。coss使用异步写操作,以便主进程不会在磁盘I/O上阻塞。
象其他文件系统一样,coss也使用块大小概念。在7.1.4章里,我谈到了文件号码。每个cache目标有一个文件号码,以便用于定 位磁盘中的数据。对coss来说,文件号码与块号码一样。例如,某个cache目标,其交换文件号码等于112,那它在coss文件系统中就从第112块 开始。因此coss不分配文件号码。某些文件号码不可用,因为cache目标通常在coss文件里占用了不止一个块。
coss块大小在cache_dir选项中配置。因为的文件号码仅仅24位,块大小决定了coss缓存目录的最大size:size = 块大小 x (2的24次方)。例如,对512字节的块大小,你能在coss cache_dir中存储8GB数据。
coss不执行任何正常的cache置换算法(见7.5章)。代替的,cache命中被"移动"到循环文件的末尾。这本质上是LRU算法。不幸的是,它确实意味着cache命中导致磁盘写操作,虽然是间接的。
在coss中,没必要去删除cache目标。简单的忘记无用目标所分配的空间。当循环文件的终点再次抵达该空间时,它就被重新利用。
8.6.2 编译和配置coss
为了使用coss,你必须在运行./configure时,在--enable-storeio列表里增加它:
% ./configure --enable-storeio=ufs,coss ...
coss缓存目录要求max-size选项。它的值必须少于stripe大小(默认1MB,但可以用--enable-coss-membuf-size选项来配置)。也请注意你必须忽略L1和L2的值,它们被ufs基础的文件系统使用。如下是示例:
cache_dir coss /cache0/coss 7000 max-size=1000000
cache_dir coss /cache1/coss 7000 max-size=1000000
cache_dir coss /cache2/coss 7000 max-size=1000000
cache_dir coss /cache3/coss 7000 max-size=1000000
cache_dir coss /cache4/coss 7000 max-size=1000000
甚至,你可以使用block-size选项来改变默认的coss块大小。
cache_dir coss /cache0/coss 30000 max-size=1000000 block-size=2048
关于coss的棘手的事情是,cache_dir目录参数(例如/cache0/coss)实际上不是目录,它是打开或创建的常规文件。所以你可以用裸设备作为coss文件。假如你错误的创建coss文件作为目录,你可以在启动时见到如下错误:
2003/09/29 18:51:42| /usr/local//var/cache: (21) Is a directory
FATAL: storeCossDirInit: Failed to open a coss file.
因为cache_dir参数不是目录,你必须使用cache_swap_log指令(见13.6章)。否则试图在cache_dir目录中创建swap.state文件。在该情形下,你可以见到这样的错误:
2003/09/29 18:53:38| /usr/local//var/cache/coss/swap.state:
(2) No such file or directory
FATAL: storeCossDirOpenSwapLog: Failed to open swap log.
coss使用异步I/O以实现更好的性能。实际上,它使用aio_read()和aio_write()系统调用。这点也许并非在所有操作系统中 可用。当前它们可用在FreeBSD,Solaris,和Linux中。假如coss代码看起来编译正常,但你得到"Function not implemented"错误消息,那就必须在内核里激活这些系统调用。在FreeBSD上,必须在内核配置文件中有如下选项:
options VFS_AIO
8.6.3 coss发行
coss还是实验性的功能。没有充分证实源代码在日常使用中的稳定性。假如你想试验一下,请做好存储在coss cache_dir中的资料丢失的准备。 从另一面说,coss的初步性能测试表现非常优秀。示例请见附录D。
coss没有很好的支持从磁盘重建cache数据。当你重启时,你也许会发现从swap.state文件读取数据失败,这样就丢失了所有的缓存数据。甚至,在重启后,不能记忆它在循环文件里的位置。它总是从头开始。
coss对目标置换采用非标准的方式。相对其他存储机制来说,这可能导致命中率更低。
某些操作系统在单个文件大于2GB时,会有问题。假如这样的事发生,你可以创建更多小的coss区域。例如:
cache_dir coss /cache0/coss0 1900 max-size=1000000 block-size=128
cache_dir coss /cache0/coss1 1900 max-size=1000000 block-size=128
cache_dir coss /cache0/coss2 1900 max-size=1000000 block-size=128
cache_dir coss /cache0/coss3 1900 max-size=1000000 block-size=128
使用裸磁盘设备(例如/dev/da0s1c)也不会工作得很好。理由之一是磁盘设备通常要求I/O发生在512个字节的块边界(译者注:也就是块设备访问)。另外直接的磁盘访问绕过了系统高速缓存,可能会降低性能。然而,今天的许多磁盘驱动器,已经内建了高速缓存。
8.7 null存储机制
Squid有第5种存储机制叫做null。就像名字暗示的一样,这是最不健壮的机制。写往null cache_dir的文件实际上不被写往磁盘。
大多数人没有任何理由要使用null存储系统。当你想完全禁止的磁盘缓存时,null才有用。你不能简单的从.conf文件 里删除所有cache_dir行,因为这样的话会增加默认的ufs cache_dir。null存储系统有些时候在测试,和压力测试时有用。既然文件系统是典型的性能瓶颈,使用null存储机制能获取基于当前 硬件的的性能上限。
为了使用该机制,在运行./configure时,你必须首先在--enable-storeio列表里指定它:
% ./configure --enable-storeio=ufs,null ...
然后在.conf里创建cache_dir类型为null:
cache_dir /tmp null
也许看起来有点奇怪,你必须指定目录给null存储机制。使用目录名字作为cache_dir标识符。例如,你能在cache管理器的输出里见到它。
8.8 哪种最适合我?
Squid的存储机制选择看起来有点混乱和迷惑。aufs比diskd更好?我的系统支持aufs或coss吗?假如我使用新的机制,会丢失数据吗?可否混合使用存储机制?
首先,假如Squid轻度使用(就是说每秒的请求数少于5个),默认的ufs存储机制足够了。在这样的低请求率中,使用其他存储机制,你不会观察到明显的性能改进。
假如你想决定何种机制值得一试,那你的操作系统可能是个决定因素。例如,aufs在Linux和Solaris上运行良好,但看起来在其他系统中有问题。另外,coss代码所用到的函数,当前不可用在某些操作系统中(例如NetBSD)。
从我的观点看来,高性能的存储机制在系统崩溃事件中,更易受数据丢失的影响。这就是追求最好性能的权衡点。然而对大多数人来说,cache数据相 对价值较低。假如的缓存因为系统崩溃而破坏掉,你会发现这很容易,只需简单的newfs磁盘分区,让cache重新填满即可。如果你觉得替换 Squid的缓存内容较困难或代价很大,你就应该使用低速的,但可信的文件系统和存储机制。
近期的Squid允许你对每个cache_dir使用不同的文件系统和存储机制。然而实际上,这种做法是少见的。假如所有的cache_dir使用相同的size和相同的存储机制,可能冲突更少。
第9章 Cache拦截
Cache拦截是让传输流向Squid的流行技术,它不用配置任何客户端。你可以配置路由器或交换机将HTTP连接转发到运行的主机。 运行的操作系统被配置成接受外部数据包,并将其递交给进程。为了让HTTP拦截生效,你必须配置3个独立的因素:网络设备, 运行的操作系统,和自身。
(译者注:Cache拦截实际上指的是Squid的透明代理) 9.1它如何工作? Cache拦截包含了某些网络欺骗,它对理解在客户端和Squid之间的会话有用。我使用图9-1和如下的tcpdump示例输出,来解释当数据包通过网络时,如何被拦截。 1.用户代理(user-agent)想请求某个资源,它对原始服务器发起index.html请求,例如:。它需要原始服务器的IP地址,所以先发起一个DNS请求: Packet 1 TIME: 19:54:41.317310 UDP: 206.168.0.3.2459 -> 206.168.0.2.53 DATA: .d................ --------------------------------------------------------------------------- Packet 2 TIME: 19:54:41.317707 (0.000397) UDP: 206.168.0.2.53 -> 206.168.0.3.2459 DATA: .d........................PR.....%........PR. ....$........PR...ns1.sonic.net.........PR...ns2.Q........PR ...ns...M...............h.............!.z.......b...... 2.现在有了IP地址,用户代理初始化到原始服务器80端口的TCP连接: Packet 3 TIME: 19:54:41.320652 (0.002945) TCP: 206.168.0.3.3897 -> 208.201.239.37.80 Syn DATA: 3.路由器或交换机注意到目的地址是80端口的TCP SYN包。下一步会发生什么依赖于特定的拦截技术。在4层交换和路由策略上,网络设备简单的将TCP包转发到Squid的数据链路地址。当直接 挂在网络设备上时,就这样工作。对WCCP来说,路由器封装TCP包为GRE包。因为GRE包有它自己的IP地址,它可能被通过多个子网进行路由。换句话 说,WCCP不要求直接挂在路由器上。 4.Squid主机的操作系统接受到拦截包。对4层交换来说,TCP/IP包并没有改变。 假如包使用了GRE封装,主机会剥离外部的IP和GRE头部,并将原始的TCP/IP包放在输入队列里。 注意主机接受到的包是针对外部地址的(原始服务器的)。正常情况下,这个包不匹配任何本地地址,它会被丢弃。为了让主机接受外部数据包,你必须在大多数操作系统上激活IP转发。 5.客户端的TCP/IP包被包过滤代码处理。数据包必须匹配某个规则,该规则指示内核转交这个包给。如果没有这样的规则,内核简单的将包按照它自己的方式转发给原始服务器,这不是你想要的。 注意SYN包的目的端口是80,但可能侦听在不同的端口,例如3128。包过滤规则允许你改变端口号。你不必让侦听在80端口。通过tcpdump,你能见到这步,因为转发的包不会再次通过网络接口代码。 即使侦听在80端口,包过滤器的重定向规则仍是必要的。可以让不在这些端口上接受拦截包。重定向规则有点神奇,它转交外部数据包给。 6.Squid接受到新连接的通知,它接受这个连接。内核发送SYN/ACK包返回给客户端: Packet 4 TIME: 19:54:41.320735 (0.000083) TCP: 208.201.239.37.80 -> 206.168.0.3.3897 SynAck DATA: 就象你见到的一样,源地址是原始服务器,尽管这个包不会抵达原始服务器。操作系统只是简单的将源地址和目的地址交换一下,并将它放进响应数据包里。 7.用户代理接受到SYN/ACK包,建立起完整的TCP连接。用户代理现在相信它是连接到原始服务器,所以它发送HTTP请求: Packet 5 TIME: 19:54:41.323080 (0.002345) TCP: 206.168.0.3.3897 -> 208.201.239.37.80 Ack DATA: --------------------------------------------------------------------------- Packet 6 TIME: 19:54:41.323482 (0.000402) TCP: 206.168.0.3.3897 -> 208.201.239.37.80 AckPsh DATA: GET / HTTP/1.0 User-Agent: Wget/1.8.2 Host: Accept: */* Connection: Keep-Alive 8.Squid接受HTTP请求。它使用HTTP Host头部来转换局部URL为完整的URL。在这种情形下,可在access.log文件里见到。 9.从这点开始,正常的处理请求。一般cache命中会立刻返回。cache丢失会转发到原始服务器。 10.最后,是从原始服务器接受到的响应: Packet 8 TIME: 19:54:41.448391 (0.030030) TCP: 208.201.239.37.80 -> 206.168.0.3.3897 AckPsh DATA: HTTP/1.0 200 OK Date: Mon, 29 Sep 2003 01:54:41 GMT Server: Apache/1.3.26 (Unix) PHP/4.2.1 mod_gzip/1.3.19.1a mo d_perl/1.27 P3P: policyref="",CP="C AO DSP COR CURa ADMa DEVa TAIa PSAa PSDa IVAa IVDa CONo OUR DELa PUBi OTRa IND PHY ONL UNI PUR COM NAV INT DEM CNT STA P RE" Last-Modified: Sun, 28 Sep 2003 23:54:44 GMT ETag: "1b76bf-b910-3ede86c4" Accept-Ranges: bytes Content-Length: 47376 Content-Type: text/html X-Cache: MISS from X-Cache: MISS from 10.0.0.1 Connection: keep-alive 不应该让交换机或路由器来拦截到原始服务器的连接。假如这种情况发生,结束与自己的会话,并且不能满足任何cache丢失。 防止这类转发死循环的最好方法是,确认用户和连接到交换机或路由器的独立接口。无论何时,应该在指定接口上应用拦截规则。最明显的,不该在 使用的接口上激活拦截。 9.2为何要(或不要)拦截? 许多单位发现,cache拦截很有用,因为他们不能,或不愿意配置所有用户的web浏览器。相对于配置成百上千台工作站来说,在单个交换机或路由器上做一点网络欺骗更容易。从我们面临的许多选择来看,cache拦截确实有好也有坏。它可能让你的生活更容易,但也许会更难。 Cache拦截的最明显的贡献是,所有HTTP请求通过自动离开你的网络。你不必担心配置任何浏览器,用户可能在浏览器上禁止他们的代 理设置。cache拦截让网络管理员完全控制HTTP会话。你可以改变,增加,或删除的缓存,而不会显著影响你的用户上网冲浪。 关于HTTP拦截的主要不利点就是该技术违背了TCP/IP的标准。这些协议要求路由器或交换机转发TCP/IP包到目的IP地址里指定的主机。 然而转发包到cache代理破坏了这些规则。代理伪装身份接受转交过来的连接。用户代理被欺骗了,以为它们在与真正的web服务器会话。 这样的混乱导致在老版本的Microsoft IE浏览器中产生严重问题。浏览器的Reload按钮是刷新HTML页面的最容易的方法。当浏览器被配置成使用cache代理时,reload请求包含了 一个Cache-Control:no-cache头部,它强迫产生cache丢失(或cache确认),并确保响应是最近更新的。假如没有明确配置使用 代理,浏览器会忽略该头部。当使用cache拦截时,浏览器认为它在连接到原始服务器,因此没必要发送该头部。在这种情形下,不会告知用户的 Reload按钮,也许不会验证cache响应。的ie_refresh提供了解决此bug的局部解决方法(见附录A)。Microsoft已 经在其IE 5.5 SP1中解决了这个问题。 因为类似的理由,你不能结合cache拦截使用HTTP代理验证。因为客户端不知道这个代理,它不会发送必要的Proxy- Authorization头部。另外,407(代理验证请求)响应代码也不恰当,因为响应看起来象来自原始服务器,原始服务器从来不会发送如此响应。 也不能在cache拦截中使用RFC 1413 ident查询(见6.1.2.11章节)。Squid不能对必要的IP地址建立新的TCP Socket连接。操作系统在转发拦截连接到时,它执行欺骗。然后,当希望bind新的TCP Socket到外部IP地址时,它不能执行欺骗。它想bind的地址实际上并非真正本地的,所以bind系统调用失败。 cache拦截也与设计成阻止地址欺骗的IP过滤冲突(见RFC 2267:Network Ingress Filtering: Defeating Denial of Service Attacks Which Employ IP Source AddressSpoofing)。考虑如图9-2显示的网络。路由器有2个LAN接口:lan0和lan1。网络管理员在路由器上使用包过滤器,以确保 没有内部主机传送假冒源地址的数据包。路由器只会转发源地址对应相连网络的数据包。包过滤规则也许看起来如下: # lan0 allow ip from 172.16.1.0/24 to any via lan0 deny ip from any to any via lan0 # lan1 allow ip from 10.0.0.0/16 to any via lan1 deny ip from any to any via lan1 现在看看,当路由器和lan1中的主机配置成拦截来自lan0中的HTTP连接后,会发生什么。Squid装扮成原始服务器,这意味着 从到用户的响应TCP包欺骗了源地址。lan0过滤规则导致路由器拒绝这些包。为了让cache拦截生效,网络管理员须移除lan0规则。这样 就让网络有漏洞,从而易遭拒绝服务攻击。 我在先前的章节里描述过,客户端在打开连接之前必须先进行DNS查询。在某些防火墙环境中,这样做可能有问题。你想进行HTTP拦截的主机必须能 够查询DNS。如果客户端了解自己正使用代理(因为手工配置或代理自动配置),它通常就不去解析主机名。代替的,它简单的将完整URL转发给, 由来查询原始服务器的IP地址。 另一个小问题是,接受任意目的IP地址的连接。例如,某个web站点当机了,但它仍然有DNS记录存在。伪装这个站点接受TCP连接。客户端会认为该站点仍然在运行,因为连接有效。当连接到原始服务器失败时,它强迫返回错误消息。 万一形势不清,HTTP拦截在初次使用时有些棘手或困难。许多不同的组件必须组合工作,并且要配置正确。甚至,从内存中恢复整个配置也很困难。我强烈建议你在将其应用于生产环境之前,先建立测试环境。一旦你让它正常运行,请记录每一步细节。 9.3 网络设备 现在你了解了cache拦截的相关细节,让我们看看如何实际让它工作。我们先配置网络设备,它们用来拦截HTTP连接。 9.3.1 内置Squid 在该配置中,你无需交换或网络路由设备来拦截HTTP连接。代替的,运行的Unix系统,也就是路由器(或网桥),请见图9-2。 该配置本质上跳过了9.1章的头三步。主机充当网络路由器,它接受HTTP连接包。假如你采用此方法,请直接跳到9.4章。 9.3.2 四层交换 许多单位使用四层交换机来支持HTTP拦截。这些产品提供更多的功能,例如健壮性检测和负载均衡。我在这里仅仅讲讲拦截。关于健壮性检测和负载均 衡的信息,请见O'Reilly's Server Load Balancing and Load Balancing Servers, Firewalls, and Caches (John Wiley & Sons). 下面的章节包含了许多产品和技术的示例配置。 9.3.2.1 Alteon/Nortel 下面的配置来自ACEswitch 180和Alteon's WebOS 8.0.21。网络设置请见图9-4。 客户端连接到端口1,通过端口2连接到因特网,运行在端口3。下面的行是交换机的/cfg/dump命令的输出。你无须敲入所有这些 行。甚至,在Alteon的新版软件里,某些命令可能改变了。注意Alteon把这个功能叫做Web Cache重定向(WCR)。如下是处理步骤: 1.首先,你必须分配给Alteon交换机一个IP地址。这是必要的,以便交换机能检查的存活状态。 /cfg/ip/if 1 ena addr 172.16.102.1 mask 255.255.255.0 broad 172.16.102.255 2.Alteon的WCR属于服务负载均衡(SLB)配置。所以,必须使用如下命令在交换机上激活SLB功能: /cfg/slb on 3. 现在,用的IP地址定义real server: /cfg/slb/real 1 ena rip 172.16.102.66 4. 必须定义一个组,并分配给real server一个组号: /cfg/slb/group 1 health tcp add 1 5. 下一步定义2个过滤规则。第1条规则匹配HTTP连接(目的端口是80的TCP包),并重定向它们到组1里的server。第2条规则匹配所有其他数据包,并正常转发它们。 /cfg/slb/filt 1 ena action redir sip any smask 0.0.0.0 dip any dmask 0.0.0.0 proto tcp sport any dport http group 1 rport 0 /cfg/slb/filt 224 ena action allow sip any smask 0.0.0.0 dip any dmask 0.0.0.0 proto any 6. 最后一步是给SLB配置指定的交换端口。在端口1上,处理客户端连接(这也是客户端连接的端口),并增加2条过滤规则。在端口2上,仅须配置它正常服务(例如,向上连接到Internet): cfg/slb/port 1 client ena filt ena add 1 add 224 /cfg/slb/port 2 server ena 为了验证HTTP拦截配置正确并工作良好,你可以使用/stats/slb和/info/slb菜单里的命令。/info/slb/dump是快速有效的查看整个SLB配置的方法: >> Main# /info/slb/dump Real server state: 1: 172.16.102.66, 00:c0:4f:23:d7:05, vlan 1, port 3, health 3, up Virtual server state: Redirect filter state: 1: dport http, rport 0, group 1, health tcp, backup none real servers: 1: 172.16.102.66, backup none, up Port state: 1: 0.0.0.0, client filt enabled, filters: 1 224 2: 0.0.0.0, server filt disabled, filters: empty 3: 0.0.0.0 filt disabled, filters: empty 在该输出里,注意到交换机显示Squid在端口3上可到达,并且运行正常。你也能见到过滤规则1应用到端口1。在端口状态节里,端口1定义为客户端连接端口,端口2简单的标记为服务端口。 /stats/slb/real命令显示real server()的有用统计: >> Main# /stats/slb/real 1 ------------------------------------------------------------------ Real server 1 stats: Health check failures: 0 Current sessions: 41 Total sessions: 760 Highest sessions: 55 Octets: 0 大部分统计与任务(例如TCP连接)数量相关。假如再次运行该命令,总共的任务计数会增加。 最后,/stats/slb/group命令显示几乎同样的信息: >> Main# /stats/slb/group 1 ------------------------------------------------------------------ Real server group 1 stats: Current Total Highest Real IP address Sessions Sessions Sessions Octets ---- --------------- -------- ---------- -------- --------------- 1 172.16.102.66 65 2004 90 0 ---- --------------- -------- ---------- -------- --------------- 65 2004 90 0 假如不止1个real server在组里,该输出会更有趣。 9.3.2.2 Foundry 下面的配置示例来自ServerIron XL,运行的软件版本是07.0.07T12。跟前面一样,客户端在端口1,Internet连接在端口2,运行在端口3。然而,这样的配置少 了点东西,因为这里可以激活HTTP全局拦截。Foundry的cache拦截的名字叫做Transparent Cache Switching(TCS)。请参考图9-4。 首先请给交换机分配1个IP地址,以便执行健壮性检测: ip address 172.16.102.1 255.255.255.0 Foundry允许你在特定端口上激活或禁用TCS。然而简单起见,这里全局激活它: ip policy 1 cache tcp http global 在该行里,cache是针对TCS功能的关键字。下1行定义web cache,我定义其名字为1,并且告诉交换机它的IP地址: server cache-name 1 172.16.102.66 最后的步骤是将web cache加进cache组里: server cache-group 1 cache-name 1 假如在转发连接时有问题,请参阅show cache-group命令的输出: ServerIron#show cache-group Cache-group 1 has 1 members Admin-status = Enabled Active = 0 Hash_info: Dest_mask = 255.255.255.0 Src_mask = 0.0.0.0 Cache Server Name Admin-status Hash-distribution 1 6 3 HTTP Traffic From <-> to Web-Caches Name: 1 IP: 172.16.102.66 State: 6 Groups = 1 Host->Web-cache Web-cache->Host State CurConn TotConn Packets Octets Packets Octets Client active 441 12390 188871 15976623 156962 154750098 Web-Server active 193 11664 150722 151828731 175796 15853612 Total 634 24054 339593 167805354 332758 170603710 某些输出有些模糊,但通过重复该命令,并且观察计数器的增长,你能了解拦截是否在进行。 show server real提供几乎同样的信息: ServerIron#show server real 1 Real Servers Info Name : 1 Mac-addr: 00c0.4f23.d705 IP:172.16.102.66 Range:1 State:Active Wt:1 Max-conn:1000000 Src-nat (cfg:op):(off:off) Dest-nat (cfg:op):(off:off) 1 is a TRANSPARENT CACHE in groups 1 Remote server : No Dynamic : No Server-resets:0 Mem:server: 02009eae Mem:mac: 045a3714 Port State Ms CurConn TotConn Rx-pkts Tx-pkts Rx-octet Tx-octet Reas ---- ----- -- ------- ------- ------- ------- -------- -------- ---- http active 0 855 29557 379793 471713 373508204 39425322 0 default active 0 627 28335 425106 366016 38408994 368496301 0 Server Total 1482 57892 804899 837729 411917198 407921623 0 最后,使用show logging命令来观察交换机是否显示正常或异常: ServerIron#show logging ... 00d00h11m51s:N:L4 server 172.16.102.66 1 port 80 is up 00d00h11m49s:N:L4 server 172.16.102.66 1 port 80 is down 00d00h10m21s:N:L4 server 172.16.102.66 1 port 80 is up 00d00h10m21s:N:L4 server 172.16.102.66 1 is up 注意ServerIron认为服务运行在80端口。以后你会见到运行在3128端口的示例。包过滤规则实际上将包的目的地址从80改变为3128。这导致一些与状态检测有关的有趣结果,我在9.3.2.5节里会讲到。 9.3.2.3 Extreme Networks 在该示例里,硬件是Summit1i,软件版本是6.1.3b11。再次将客户端分配在端口1,Internet在端口2,在端口3。网络配置见图9-5。 Extreme交换机仅仅对在不同子网间进行路由的数据包进行HTTP连接的拦截。换句话说,如果你配置Extreme交换机使用二层模式(单一 VLAN里),就不能将包转发给。为了让HTTP拦截正常工作,必须给用户,Squid,和Internet配置不同的VLAN。 configure Default delete port 1-8 create vlan Users configure Users ip 172.16.102.1 255.255.255.192 configure Users add port 1 create vlan Internet configure Internet ip 172.16.102.129 255.255.255.192 configure Internet add port 2 create vlan Squid configure Squid ip 172.16.102.65 255.255.255.192 configure Squid add port 3 下一步是激活和配置交换机的路由: enable ipforwarding configure iproute add default 172.16.102.130 最后,配置交换机重定向HTTP连接到Squid: create flow-redirect http tcp destination any ip-port 80 source any configure http add next-hop 172.16.102.66 9.3.2.4 Cisco Arrowpoint 下类配置基于我以前的测试笔记。然而,最近我没有使用这类型的交换机,不能确保如下命令仍然正确: circuit VLAN1 ip address 172.16.102.1 255.255.255.0 service pxy1 type transparent-cache ip address 172.16.102.66 port 80 protocol tcp active owner foo content bar add service pxy1 protocol tcp port 80 active 9.3.2.5 关于HTTP服务和健壮性检测的评论 在上面的示例里,路由器/交换机都直接转发包,不会改变目的TCP端口。在9.4章里用到的包过滤规则改变了目的端口。如果试图在同一主机上运行HTTP服务和,那么就产生了一个有趣的问题。 为了在3128端口运行Squid的同时,还要在80端口运行HTTP,包过滤配置必须有1条特殊的规则,它接受到本机HTTP服务的TCP连 接。否则,连接会直接转交给Squid。该规则易于建立。假如目的端口是80,并且目的地址是服务器的,那么主机正常接受这个包。然而所有的拦截包有外部 目的地址,所以它们不会匹配该规则。 然而,当路由器/交换机进行HTTP健壮性检测时,它连接到服务器的IP地址。这样,健壮性检测的数据包匹配了上述规则,它不会转交给Squid。路由器/交换机检测了错误的服务。假如HTTP服务down掉了,而还在运行,那健壮性检测就产生错误结果。 解决这个问题的一些选择是: 1.不要在Squid主机上运行HTTP服务; 2.增加1条特殊的包过滤规则,将来自路由器/交换机的状态检测的包转交给; 3.配置路由器/交换机,改变目的端口为3128; 4.禁止4层状态检测。 9.3.3 Cisco策略路由 策略路由与4层交换并非不同。它在Cisco和其他公司的路由产品上执行。主要的区别是策略路由不包括任何健壮性检测。这样,假如超载 或完全不可响应,路由器还会继续转发包到,而不是将包路由到原始服务器。策略路由要求位于路由器直接相连的子网中。 在本示例里,使用了Cisco 7204路由器,运行IOS Version 12.0(5)T。网络配置与前面的一样,见图9-5。 首先的配置步骤是定义访问列表,匹配来自客户端的到80端口的数据包。必须确保Squid发起的到80端口的数据包不会被再次拦截。做到这点的方法之一是,定义1个特殊规则,拒绝来自的数据包,紧跟1条规则允许所有其他的数据包: access-list 110 deny tcp host 172.16.102.66 any eq www access-list 110 permit tcp any any eq www 另外,如果Squid和用户位于不同的子网,你可以仅仅允许来自用户所在网络的数据包: access-list 110 permit tcp 10.102.0.0 0.0.255.255 any eq www 下一步是定义路由映射。在这里你告诉路由器转发拦截包到何处去: route-map proxy-redirect permit 10 match ip address 110 set ip next-hop 172.16.102.66 这些命令表明,“假如IP地址匹配访问列表110,就转发该包到172.16.102.66”。在route-map行的数字10是一个序列号,假如你有多个路由映射的话。最后一步是应用路由映射到客户端连接的接口: interface Ethernet0/0 ip policy route-map proxy-redirect IOS不对策略路由提供很多调试方法。然而,show route-map命令足够可用: router#show route-map proxy-redirect route-map proxy-redirect, permit, sequence 10 Match clauses: ip address (access-lists): 110 Set clauses: ip next-hop 172.16.102.66 Policy routing matches: 730 packets, 64649 bytes 9.3.4 Web Cache Coordination协议 Cisco对4层交换技术的响应叫做Web Cache Coordination Protocol(WCCP).WCCP在许多方面与典型的4层拦截不同。 首先,拦截包被封装在GRE(路由封装类)里。这点允许数据包跨子网传输,也就意味着不必直接连在路由器上。因为数据包是封装的,主机必须对其进行解包。并非所有的Unix系统有解开GRE包的代码。 第二个不同是,路由器如何决定将负载分摊到多个cache上。事实上,路由器不做这个决定,由cache来做。当路由器有一组支持WCCP的 cache时,其中一个cache主动成为组的领导。由领导cache来决定如何分摊负载和通知路由器。在路由器重定向然任何连接之前,这是一个额外的必 要步骤。 因为WCCP使用GRE,路由器可能强迫要求将来自HTTP请求的大TCP包分割成片断。幸运的是,这点不会经常发生,因为大部分HTTP请求比 以太网MTU size(1500字节)要小。默认的TCP和IP包头部是20字节,这意味着以太网帧能实际携带1460字节的数据。GRE封装在GRE头部增加了20 字节,另外在第二个IP头部增加了20字节。这样来自客户端的正常的1500字节的TCP/IP包,在封装后变成了1540字节。这样数据包就太大而不能 在单个以太网帧里传输,所以路由器将原始包分割成两个片断。 9.3.4.1 WCCPv1 该节的配置示例在运行IOS Version 12.0(5)T的Cisco 7204路由器上测试。网络配置跟图9-5同。 首先,在IOS配置中输入如下两行,激活路由器的WCCP: ip wccp version 1 ip wccp web-cache 接着,必须在单独的路由器接口上激活WCCP。在HTTP包离开路由器的接口上激活WCCP,也就是路由器连接到外部原始服务器或Internet网关的接口: interface Ethernet0/1 ip address 172.16.102.129 255.255.255.192 ip wccp web-cache redirect out 请确认保存了配置改变。 你也许想使用访问列表来阻止某些web站点的拦截。可以使用访问列表来防止循环转发。例如: ! don't re-intercept connections coming from Squid: access-list 112 deny tcp host 172.16.102.66 any eq www ! don't intercept this broken web site access-list 112 deny tcp any 192.16.8.7 255.255.255.255 eq www ! allow other HTTP traffic access-list 110 permit tcp any any eq www ip wccp web-cache redirect-list 112 路由器不发送任何数据到,直到宣称它自己是路由器。我在9.5.1节里解释如何配置的WCCP。 9.3.4.2 WCCPv2 当前标准的Squid发布仅支持WCCPv1。然而,可在squid -cache.org/" target="_blank">http://devel.-cache.org/找到WCCPv2的补丁。该代码仍是实验性的。 注意从路由器发送到Squid的GRE包,包含了额外的4个字节。WCCPv2在GRE头部和封装的IP包之间,插入了一个重定向头部。也许需要修改内核代码来计算这个额外的头部。 9.3.4.3 调试 IOS提供许多命令来监视和调试WCCP。show ip wccp web-cache命令提供一些基本的信息: router#show ip wccp web-cache Global WCCP information: Router information: Router Identifier: 172.16.102.129 Protocol Version: 1.0 Service Identifier: web-cache Number of Cache Engines: 1 Number of routers: 1 Total Packets Redirected: 1424 Redirect access-list: -none- Total Packets Denied Redirect: 0 Total Packets Unassigned: 0 Group access-list: -none- Total Messages Denied to Group: 0 Total Authentication failures: 0 欲了解更多细节,在前叙命令后加一个detail单词: router#show ip wccp web-cache detail WCCP Cache-Engine information: IP Address: 172.16.102.66 Protocol Version: 0.4 State: Usable Initial Hash Info: 00000000000000000000000000000000 00000000000000000000000000000000 Assigned Hash Info: FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF Hash Allotment: 256 (100.00%) Packets Redirected: 1424 Connect Time: 00:17:40 这里可以看到的IP地址和状态。假如不止一个cache与路由器会话,那么hash分配信息看起来不同。大多数情况下,每个cache接受到相等的hash值。 注意第二条命令输出的协议版本值,与第一条命令的不一样。不幸的是,赋予了版本号太多的意义。show ip wccp web-cache命令看起来报告WCCP协议的主版本号(例如1或2),然而show ip wccp web-cache detail的版本号看起来匹配Squid的wccp_version指令的值。 9.4 操作系统配置 为了让cache拦截正常工作,必须在操作系统中激活某些网络功能。首先,必须激活IP包转发。这就允许操作系统接受目的地址是外部IP的数据包。接着,必须激活和配置内核中的相关代码,以重定向外部包到Squid。 9.4.1 Linux 本节的指导适合2.4系列Linux内核。我使用RedHat Linux7.2(内核是2.4.7-10)。假如你使用的版本不同,那可能不能运行。建议搜索下Squid的FAQ或其他地方,找到关于内核的更新的或历史的信息。 在我测试iptables过程中,不必激活IP转发。然而,你也可以试试在一开始就激活它,并在一切运行良好后,看看能否禁掉它。激活包转发的最好的方法是在/etc/sysctl.conf文件里增加如下行: net.ipv4.ip_forward = 1 一般来说,在HTTP拦截生效前,不必编译新内核。假如你不知道如何配置和创建新Linux内核,请参阅O'Reilly's Running Linux by Matt Welsh, Matthias Kalle Dalheimer, and Lar Kaufman。当你配置内核时,请确认如下选项被激活: o General setup Networking support (CONFIG_NET=y) Sysctl support (CONFIG_SYSCTL=y) o Networking options Network packet filtering (CONFIG_NETFILTER=y) TCP/IP networking (CONFIG_INET=y) Netfilter Configuration Connection tracking (CONFIG_IP_NF_CONNTRACK=y) IP tables support (CONFIG_IP_NF_IPTABLES=y) Full NAT (CONFIG_IP_NF_NAT=y) REDIRECT target support (CONFIG_IP_NF_TARGET_REDIRECT=y) o File systems /proc filesystem support (CONFIG_PROC_FS=y) 另外,请确认该选项没被激活: o Networking options Fast switching (CONFIG_NET_FASTROUTE=n) 重定向外部数据包到的代码是Netfilter软件的一部分。如下是发送HTTP拦截连接到的规则: iptables -t nat -A PREROUTING -i eth0 -p tcp --dport 80 -j REDIRECT --to-port 3128 Linux内核维护许多不同的tables。-t nat选项表明我们正在修改NAT(网络地址转换)表。本质上,我们使用iptables将原始服务器的TCP/IP地址转换为的本地TCP/IP地址。 每个iptables表有许多链。-A PREROUTING表明我们增加了一条规则到内建的链叫做PREROUTING。PREROUTING链仅对从外部网络进入系统的数据包有效。 接下来的三个选项决定哪个包匹配该规则。-i eth0选项限制规则仅对eth0接口上接受的数据包有效。-p tcp选项指定TCP包,--dport 80指定包的目的端口是80。假如这三个条件都是true,那么包匹配该规则。 -j REDIRECT选项表明对匹配规则的包,采取何种动作。REDIRECT是内建的动作名,它导致iptables改变包的目的地址为127.0.0.1。--to-port 3128选项也指示iptables改变目的TCP端口为3128。 假如你在主机上运行HTTP服务(例如Apache),就必须增加另外的iptables规则。该必要规则允许连接到HTTP服务。否 则,REDIRECT规则导致iptables转发连接到的3128端口。可以使用-I选项在列表顶部插入一条新规则: iptables -t nat -I PREROUTING -i eth0 -p tcp -d 172.16.102.66 --dport 80 -j ACCEPT 一旦确认所有iptables规则工作正确,记得运行如下命令来保存配置: /sbin/service iptables save 将当前规则保存到/etc/sysconfig/iptables,当系统重启时,这些规则自动载入。 9.4.1.1 Linux和WCCP 2.4版本的Linux内核自带1个GRE伪装接口。然而,它不能解码WCCP任务里封装的GRE包。问题看起来在于路由器设置了WCCP/GRE包的协议类型域为0x883E。Linux的GRE驱动不知道如何处理这类型包,因为它不了解0x883E类型的协议。 可以试试给Linux打GRE模块的补丁,以便它能在WCCP下工作。Squid FAQ包含了对这个补丁的链接。然而,假如使用WCCP指定的Linux模块,事情会容易些。可以在这里找到它:-" target="_blank"> cache.org/WCCP-support/Linux/ip_wccp.c 必须编译ip_wccp.c文件为可装载内核模块。有点棘手的是,依赖于内核版本的不同,编译选项可能不同。你可以进入内核源代码目录,敲入make modules并观察编译器命令的滚动。然后拷贝这些命令中的一个,然后改变最后一个参数为ip_wccp.c。如下是我在2.4.7-10 Linux内核中使用的命令: % gcc -Wall -D_ _KERNEL_ _ -I/usr/src/linux-2.4.7-10/include \ -DMODULE -DMODVERSIONS -DEXPORT_SYMBAB \ -include /usr/src/linux-2.4.7-10/include/linux/modversions.h \ -O2 -c ip_wccp.c gcc命令在当前目录会生成ip_wccp.o文件。下一步使用insmod命令,装载模块到内核中: # insmod ip_wccp.o 注意ip_wccp模块接受来自任何源地址的GRE/WCCP包。换句话说,恶意用户可能发送数据到 cache。假如使用该模块,应该安装一条iptables规则,拒绝外部的GRE包。例如: # iptables -A INPUT -p gre -s 172.16.102.65 -j ACCEPT # iptables -A INPUT -p gre -j Drop 不要忘记敲入/sbin/service iptables save命令来保存配置。 9.4.2 FreeBSD 本节的例子基于FreeBSD-4.8,并可以在任何FreeBSD-4和5系列的后续版本上运行。 要激活IP包转发,在/etc/sysctl.conf中增加如下行: net.inet.ip.forwarding=1 需要在内核中激活2个特殊选项。假如你不知道如何编译内核,参见FreeBSD Handbook第9章(). 编辑内核配置文件,确保有如下行: options IPFIREWALL options IPFIREWALL_FORWARD 假如主机位于无人照看的机房中,我也推荐使用IPFIREWALL_DEFAULT_TO_ACCEPT选项。假如你被防火墙的规则困扰,仍然可以登陆系统中。 ipfw命令告诉内核重定向拦截连接到: /sbin/ipfw add allow tcp from 172.16.102.66 to any out /sbin/ipfw add allow tcp from any 80 to any out /sbin/ipfw add fwd 127.0.0.1,3128 tcp from any to any 80 in /sbin/ipfw add allow tcp from any 80 to 172.16.102.66 in 第一条规则匹配主机发起的数据包。它确保外出的TCP连接不会被重新定向回。第二条规则匹配响应客户端的数据包。 我在这里列出它,因为随后会有另外的ipfw规则,这些规则会拒绝这些包。第三条规则实际重定向进来的连接到。第四条规则匹配从原始服务器返回 的数据包。这里又一次假设随后会有相应的拒绝规则。 假如在主机上运行HTTP服务,就必须增加另外的规则,它放过,而不是重定向,目的地址是原始服务器的TCP包。下列规则在fwd规则之前: /sbin/ipfw add allow tcp from any to 172.16.102.66 80 in FreeBSD典型的将ipfw规则存储在/etc/rc.firewall里。一旦你确认规则设置正确,记得保存它们。将如下行加入/etc/rc.local文件,让FreeBSD在启动时自动运行/etc/rc.firewall脚本。 firewall_enable="YES" 9.4.2.1 FreeBSD和WCCP FreeBSD版本4.8和后续版本内建了对GRE和WCCP的支持。早期的版本需要补丁,你可以在这里找到: -cache.org/" target="_blank">cache.org/WCCP-support/FreeBSD/ . 内建代码的性能非常好,它是真正的内核组织编写的。可能也需要编译支持GRE的新内核。将如下行加入内核配置文件里: pseudo-device gre 对Freebsd-5,使用device代替了pseudo-device。当然,你也需要前面章节里提到的FIREWALL选项。 在安装和重启了新内核后,必须配置GRE通道来接受来自路由器的GRE包。例如: # ifconfig gre0 create # ifconfig gre0 172.16.102.66 172.16.102.65 netmask 255.255.255.255 up # ifconfig gre0 tunnel 172.16.102.66 172.16.102.65 # route delete 172.16.102.65 ifconfig命令在gre0接口上,增加了一个到路由器(172.16.102.65)的路由表入口。我发现必须删除该路由,以便能与其他路由器会话。 你也许想或必须对来自路由器的GRE包,增加一条ipfw规则: /sbin/ipfw add allow gre from 172.16.102.65 to 172.16.102.66 9.4.3 OpenBSD 本节的示例基于OpenBSD 3.3 为了激活包转发,在/etc/sysctl.conf文件里增加该行: net.inet.ip.forwarding=1 现在,在/etc/pf.conf文件里增加如下类似行,配置包过滤规则: rdr inet proto tcp from any to any port = www -> 127.0.0.1 port 3128 pass out proto tcp from 172.16.102.66 to any pass out proto tcp from any port = 80 to any pass in proto tcp from any port = 80 to 172.16.102.66 假如你没有使用OpenBSD的包过滤器,需要在/etc/rc.conf.local文件里增加一行来激活它: pf=YES 9.4.3.1 OpenBSD和WCCP 首先,增加如下行到/etc/sysctl.conf文件,告诉系统接受和处理GRE和WCCP包: net.inet.gre.allow=1 net.inet.gre.wccp=1 然后,用如下命令配置GRE接口: # ifconfig gre0 172.16.102.66 172.16.102.65 netmask 255.255.255.255 up # ifconfig gre0 tunnel 172.16.102.66 172.16.102.65 # route delete 172.16.102.65 跟Freebsd一样,我发现必须删除ifconfig自动产生的路由。最后,依赖于包过滤器的配置,必须增加一条规则以允许GRE包: pass in proto gre from 172.16.102.65 to 172.16.102.66 9.4.4 在NetBSD和其他系统上的IPFilter 本节的示例基于NetBSD 1.6.1。它们也能运行在Solaris,HP-UX,IRIX,和Tru64上,既然这些系统本身就配备了IPFilter. 激活NetBSD的包转发,将如下行加进/etc/sysctl.conf: net.inet.ip.forwarding=1 然后,将如下行插入NAT配置文件/etc/ipnat.conf中: rdr fxp0 0/0 port 80 -> 172.16.102.66 port 3128 tcp 你的接口名可能与本例的fxp0不同。 9.4.4.1 NetBSD和WCCP 我不能在NetBSD上运行WCCP,即使打了GRE补丁来接受WCCP包。该问题看起来根源在IPFilter rdr规则阻塞了特定的端口。来自路由器的包通过NetBSD的gre0接口(在这里它们被解包)。然而,包回到路由器时,走另一条通道,未被封装并且不 走同一网络接口。这样,IPFilter代码没有将的本地IP地址转换回原始服务器的地址。 9.5 配置Squid 假如你使用Linux 2.4和iptables,在运行./configure时,可使用--enable-linux-netfilter选项。它激活某些Linux的特殊 代码,以发现发起请求的原始服务器的IP地址。Squid正常情况下从Host头部,得到原始服务器的名字(和/或地址)。--enable-linux -netfilter功能仅对没有Host头部的请求来说是必要的。统计显示几乎所有的请求有Host头部,所以实际中可以不使用--enable- linux-netfilter选项。 假如正在使用IPFilter包(NetBSD,Solaris,或其他),因为同样的理由,你应该使用--enable-ipf- transparent选项。在OpenBSD上,请使用--enable-pf-transparent选项。每次运行./configure时,必须 重编译,见3.8章的描述。 在运行了./configure和重编译了后,可以编辑.conf文件。作为起点,请确认下列指令定义了给定的值: httpd_accel_host virtual httpd_accel_port 80 httpd_accel_uses_host_header on httpd_accel_with_proxy on httpd_accel_single_host off http_accel_host指令是关键。它指示接受包含局部URI的HTTP请求。 httpd_accel_uses_host_header被激活,允许使用Host头部来重新构建完整URI。virtual关键字指示 在缺乏Host头部时,将原始服务器的IP地址放进URL里。 httpd_accel_with_proxy指令控制是否既接受HTTP服务(部分URI)请求,又接受代理(完整URI)请求。在 cache拦截里,它应该被激活。如果没有用户明确的配置使用做代理,那即使httpd_accel_with_proxy没被激活, 也能工作。 httpd_acces_single_host指令正常情况下被禁止,在早期版本的里,它可能被默认激活。在cache拦截里,它应明确被禁止。 假如拦截不止针对80端口,你也许该将httpd_accel_port设为0。见附录A的更多信息。 假如你没有使用WCCP,就该准备开始发起拦截会话到了。通过使用浏览器来上网冲浪,或者使用client发起测试请求,就可以放手一试。假如你使用WCCP,那么还有许多步骤要完成。 9.5.1 配置WCCPv1 路由器不发送任何会话到,直到宣称它自己是路由器。为了让那样做,在.conf中增加如下行: wccp_router 172.16.102.65 wccp_version 4 路由器有多个接口。请确认使用与相连的接口的IP地址。这点是必要的,因为来自路由器的WCCP消息,将源IP地址设置为外出接口的地址。假如源地址不匹配wccp_router值,会拒绝WCCP消息。 WCCPv1文档规定4作为协议版本号。然而,某些用户报告,Cisco IOS 11.2仅支持协议版本3。假如你使用该版本的IOS,请在.conf里改变版本号: wccp_version 3 9.6 调试问题 HTTP拦截比较复杂,因为许多不同设备必须组合正确工作。为了帮助你跟踪问题,如下是一个问题解答检查列表: 客户端数据包正在通过路由器/交换机吗? 在简单网络里,这点显而易见。你可以trace线缆并观察指示灯的活动闪烁。然而在大而复杂的网络,数据包可能走不同的路线。假如你的组织够大, 并有网络sniffer设备,就可以观察线路中web客户端的请求数据包。低技术的方法是,断开有问题的线路,并观察是否影响客户端的web浏览。 路由器/交换机配置是否正确? 你也许要再次检查路由器/交换机配置。假如你已配置了某个接口,那能否确保它正确呢? 是否新的配置真正在设备上运行?也许在你保存配置之前,路由器/交换机已重启了。在改变生效前,你或许需要reboot设备。 交换机/路由器能与主机会话吗? 能从路由器/交换机上ping通吗?大部分4层拦截配置要求网络设备和在同一子网里。登陆路由器/交换机,确认能ping通的IP地址。 交换机/路由器相信在运行吗? 许多传输拦截设备不会发送会话到,除非它们知道是健壮的。使用调试命令来预览的健壮性状态。也许会发现三层健壮性检测(例如ICMP ping)比四层检测(例如HTTP)更容易,它使网络设备更容易将标记为存活状态。 Squid实际在运行吗? 请再次确认真正在运行,特别是在系统近期重启过的情况下。 数据包正在抵达主机吗? 使用tcpdump能见到拦截的TCP连接。如下是示例: # tcpdump -n -i eth0 port 80 假如使用WCCP,请检查来自路由器的GRE包: # tcpdump -n -i eth0 ip proto gre 假如没有看到tcpdump的任何输出,则路由器/交换机可能没有发送任何数据。在这种情况下,返回到以前的建议。 注意,假如设备正使用四层健壮性检测,你可以在tcpdump的输出里见到这些。健壮性检测来自路由器/交换机的IP地址,所以它们容易被认出。 假如你见到健壮性检测,但没有其他数据,那可能意味着路由器/交换机正把的响应理解为不健壮。例如,设备可能想见到200(OK)响应,但 返回一个错误,例如401(未授权)或404(未发现)。请对access.log运行tail -f命令。 激活了IP转发吗? 请再次确认运行的操作系统配置了IP包转发。假如没有,主机可能会丢弃拦截数据包,因为目的IP地址并非本地。 配置包过滤了吗? 请确认包过滤器(例如ipfw,iptables,pf等)配置正确。当每件事都运行良好时,你能定期运行命令来显示过滤规则,并观察计数器增长。例如: # ipfw show 300 ; sleep 3; ipfw show 300 00300 86216 8480458 fwd 127.0.0.1,3128 tcp from any to any 80 in 00300 86241 8482240 fwd 127.0.0.1,3128 tcp from any to any 80 in 注意该示例在FreeBSD上,包和字节计数器(第二和第三列)正在增长。 环路接口起来和配置了吗? 假如有一条规则转发/重定向包到127.0.0.1,请确认环路接口(例如lo0,lo等)起来了,并配置过它。假如没有,内核简单的跳过这条转发/重定向规则。 WCCP/GRE包被正确解开了吗? 假如使用WCCP,请确认GRE包被正确解开。假如因为某些理由,系统不知道该如何处理GRE包,那就在netstat -s的输出里会见到"unknown/unsupported protocol"计数器在增长。 # netstat -s | grep unknown 46 packets for unknown/unsupported protocol 假如OS有GRE接口,请频繁运行netstat -i命令,观察不断增长的包数量: # netstat -in | grep ^gre0 Name Mtu Network Address Ipkts Ierrs Opkts Oerrs Coll gre0 1476 304452 0 0 4 0 另外,在GRE接口上运行tcpdump: # tcpdump -n -i gre0 Squid能响应客户端吗? 有可能路由器/交换机能发送包到,但不能将包发送回客户端。这种情况可能发生在:防火墙过滤规则拒绝外出数据包,或没有到客户端IP地址的路由。为了检查这种情况,请运行netstat -n并观察SYN_RCVD状态的sockets: % netstat -n Active Internet connections Proto Recv-Q Send-Q Local Address Foreign Address (state) tcp4 0 0 10.102.129.246.80 10.102.0.1.36260 SYN_RCVD tcp4 0 0 10.102.129.226.80 10.102.0.1.36259 SYN_RCVD tcp4 0 0 10.102.128.147.80 10.102.0.1.36258 SYN_RCVD tcp4 0 0 10.102.129.26.80 10.102.0.2.36257 SYN_RCVD tcp4 0 0 10.102.129.29.80 10.102.0.2.36255 SYN_RCVD tcp4 0 0 10.102.129.226.80 10.102.0.1.36254 SYN_RCVD tcp4 0 0 10.102.128.117.80 10.102.0.1.36253 SYN_RCVD tcp4 0 0 10.102.128.149.80 10.102.0.1.36252 SYN_RCVD 假如你看到这些,请使用ping和traceroute来确认能与客户端双向通信。 Squid能与原始服务器会话吗? 假如不能连接到原始服务器,拦截HTTP连接会无法进行。如果这点发生,netstat会显示许多连接在SYN_SENT状态: % netstat -n Active Internet connections Proto Recv-Q Send-Q Local Address Foreign Address (state) tcp4 0 0 172.16.102.66.5217 10.102.129.145.80 SYN_SENT tcp4 0 0 172.16.102.66.5216 10.102.129.224.80 SYN_SENT tcp4 0 0 172.16.102.66.5215 10.102.128.71.80 SYN_SENT tcp4 0 0 172.16.102.66.5214 10.102.129.209.80 SYN_SENT tcp4 0 0 172.16.102.66.5213 10.102.129.62.80 SYN_SENT tcp4 0 0 172.16.102.66.5212 10.102.129.160.80 SYN_SENT tcp4 0 0 172.16.102.66.5211 10.102.128.129.80 SYN_SENT tcp4 0 0 172.16.102.66.5210 10.102.129.44.80 SYN_SENT tcp4 0 0 172.16.102.66.5209 10.102.128.73.80 SYN_SENT tcp4 0 0 172.16.102.66.5208 10.102.128.43.80 SYN_SENT 再次用ping和traceroute来确认能与原始服务器会话。 外出连接正被拦截吗? 假如能ping通原始服务器,并且仍然见到大量的连接在SYN_SENT状态,那么路由器/交换机可能正在拦截的外出TCP 连接。在某些情况下,能检测到这种转发循环,并写警告日志到cache.log。如此的转发死循环能迅速耗光的所有文件描述符,这样 也会在cache.log里产生警告。 假如你怀疑这个问题,请使用client程序来发起一些简单的HTTP请求。例如,该命令发起一条直接到原始服务器的HTTP请求: % /usr/local//bin/client -p 80 -h slashdot.org / 假如该命令成功,你可以在屏幕上见到来自Slashdot站点的许多难看的HTML。然后通过来试试同样的请求: % /usr/local//bin/client -r -p 3128 -h 127.0.0.1 slashdot.org/ 假如没有在cache.log里看到错误消息,就可以再次在屏幕上看到一些HTML。假如看到转发循环错误,就必须重新配置交换机/路由器,以便它允许的外出连接正常通过,而不被拦截。