系统学习のCACHE 学习

http://www.phpfans.net/article/htmls/200911/Mjg3MDgx.html

写道
作者: admin126com 时间: 2009-11-15
距离上一篇,大约有7个月了,因为家里有了点事,所以一直没有继续写什么。今天算是续写《大型》系列,这个算是第三部分,主要的侧重点是Cache和Buffer部分。

本篇特为祝贺phpx论坛9月1日聚会圆满成功,同时献给我刚刚过世的母亲,感谢phpx论坛的朋友长时间的关心和帮助。
在《大型》(一)中,我大概地阐述了一下Cache和Buffer的含义。Cache和Buffer是系统架构中非常重要甚至是相当核心的内容。Cache叫做“缓存”,而Buffer叫做“缓冲”,接下来我们分别讨论它们的性质。

概念描述
Cache
缓存在Web架构中扮演了重要的角色。其实,Cache是一个硬件概念。它表示在数据通路中连接两类不同速度的设备的中间部分。我们最直接的印象,应该就是在认识CPU参数时,看见的L2 Cache、这里的L表示Level,L2表示二层缓存(自然还有L1 Cache)。CPU的Cache实际是连接CPU总线和内存总线的中间部分,由于CPU对数据读写的速度要大于内存的读写速度,为了使通路顺畅,需要Cache这类设备来连接这两条不等速的总线,这样它们就不会直接接触。CPU(实际上是寄存器)从内存读取数据时,是先向Cache中查找的,如果命中的话,CPU就可以以全速(部分CPU的L2是半速的,比如经典的P2)地获取数据。这在对数据要做频繁读取的操作中(比如浮点运算)会获得极大的好处。(玩电脑的时间比较长的朋友可能会记得当年的超频极品——哥斯达黎加产的Celeron 300A,其实它的超频性能不一定好过Celeron 300,但是它有128K的全速L2,这样使得它的浮点性能大大好于普通的Celeron。而且有的时候L2也是超频的瓶颈)(有的系统上还有L3 Cache,大部分是集成在主板上的)

在CPU总线之下,还有很多设备之间使用了Cache。相对于CPU对内存的读Cache,有一类Cache是另一个方向为主的写Cache(假设我们按照从高速到低速设备流动为正方向),一个典型的例子就是刻录机。现在的刻录机一般都会有2MB的Cache,部分专业设备有8MB或更大,那么这个Cache是做什么的呢?它的一端是IDE总线(也可能是SCSI或别的),另一端是实际的写入设备(假设是激光头,实际上刻录机远没这么简单),那么当IDE总线以一个相对固定的速率向刻录机发送数据流的时候,激光头是无法保证按照这个原速去写光盘的。根据光盘的质量、震动、温度、偏转角度、电机状态等物理因素,刻录机无法保证写入速度恒定,更无法保证和IDE总线带宽保持一致(哪里有那么快的刻录机……),所以需要一个设备来平衡这之间的速率矛盾,这就是刻录机的写Cache。因为IDE总线只是一条通路,并不是一个存储设备,如果刻录机直接去向IDE总线“要”数据的话,是要不来的,或者得到的不知道是什么东西,这就像是一条窄胡同只能容许一个人通过,一大队人只能站着排一个接一个地走过去。在一个不固定的时间里想截获其中一个人,当然拦住的也不一定是谁,因为走过去的人就已经走过去了,拉不回来(听起来好像黄泉路啊……好可怕)。这样一个写Cache就会扮演一个临时集合的场所——这些人都走进了一个小广场,等待被抓。由于有了这样一个可以存储数据的设备,刻录机就可以主动地“申请”数据,而Cache又可以以很高的反应效率把数据提供给刻录机,保证连续的物理写入、那么当Cache中的有效数据越来越少(广场中的人越抓越少),它就会通知IDE总线继续向Cache中灌接下来的数据(通知那个胡同继续过人)直到保持Cache中的数据写满或基本写满(广场中站满了人)。这样IDE总线无需顾及这台刻录机的物理性能究竟怎样,稳定程度有多高,它只管在应该送的时候把数据送给Cache,别的事情就不归它管了。

另一类特殊的Cache是双向的,即它既用于读取,同时也用于写入。这一般都非纯硬件设备,一个非常典型的例子就是Ramdisk上的临时文件,具体过程大家自己去考虑一下就清楚了。虽然它用软件方法模拟了一种硬件模型,其实严格意义上来说,临时文件并不属于Cache,我们暂时不去考虑它。
这样,我们总结出Cache的几条规律:

1、Cache一般是连接两个设备,通常他们的速度是不同的,或者一方是不稳定的。
2、对于Cache两端设备的数据流通方向来看,Cache一般都是单向的。
3、Cache两端的设备中的高速(匀速)一方没有存储属性。

接下来,我们来讨论Cache的另外一些性质。
Cache有一个很表象的问题,就是Cache里的数据要保存多久。这就涉及到Cache的一个重要属性——有效期。这个属性在读Cache和写Cache中的意义是不同的。在读Cache结构里,我们当然希望可以在Cache中取得尽量多的有用信息,最佳状态是每次请求都可以从Cache中获得数据,而不是从数据源中。从Cache中取得数据的成功率叫做读Cache的命中率。在通常的结构中,Cache的大小都是小于数据源的总数据量的,假设Cache的大小大于数据源的数据总量,例如一个2MB的读Cache,但是数据总量只有1MB,那么所有数据都可以被送进Cache,在这之后,数据源的使命就结束了,读取一方总是可以从Cache中获取到数据,因为如果在Cache中无法得到需要的数据,在数据源中也一定无法得到,所以这时候的命中率是100%(这当然就是最优情况了)。那么在一般的结构中,数据总有在Cache中无法取得的时候,那么读取方就要向数据源获取。一般的操作将是读取方在获取到数据之后,还会把它写入Cache以备以后使用(这和Cache策略有关,即系统关于这笔数据是否有被缓存的必要的决定)。那么向Cache中写入数据的时候,如果Cache已经满了,那么通常就需要删除一部分数据,以腾出空间来供写入,这个时候就是要决定哪些数据已经“过期”了,或者说它现在看起来最“没用”(最没有可能被近期访问到)。如果我们在开始的时候设定Cache中数据在写入5分钟后过期,那么这个时候系统就会去扫描Cache中5分钟以上没有被touch过的数据,这部分数据即已“过期”的,如果没有过期数据,系统可能会根据一个既定的LRU算法来决定哪些数据比较无用。

在写Cache中,有效期的概念相对简单,因为一般来说,它都是被只使用一次的,比如刻录机的例子,在激光头正确地写入了Cache中取来的数据之后,它就无用了,也就可以简单地认为它过期了。大不了一次写失败了,它还可以再多“活”一次。当一笔数据“死”掉之后,系统会标记这部分空间为可覆盖的,那么IDE总线再送来的数据会毫不客气地覆盖在它上面(宣布广场上的一个位置没有人,那么胡同中挤进来的下一个人不会去理会那个位置是不是有人而毫不客气地踩上去)。
综合以上,Cache的另外一些性质:

4、读Cache中的数据总是希望被更多次地访问以代替从数据源中直接读取
5、写Cache中的数据一般在正确读取一次后就无效了
6、在读Cache结构中,向Cache中写入数据一般是由读取方决定并完成的
7、在写Cache结构中,向Cache中写入数据一般是由读取方决定,由发送方完成的。
8、在读Cache中,需要的数据在Cache中存在并被访问的几率叫做命中率,写Cache中一般没有命中率概念。
9、在读Cache中,数据被保存的有效时间叫做生存期,写Cache中一般没有生存期概念。

由以上的性质,我们发现,读Cache比写Cache看上去要复杂一些,而且在很多时候(尤其是我们要解释的Web架构中),使用更多的是读Cache,所以以后的Cache,如无特殊说明,均指读Cache。
那么Cache的一般定义,我们可以简单阐述为:

一块小于数据源的存储空间,系统希望更多地访问它获取数据以代替从数据源中直接获取。

根据我们上面的描述,我们将问题带入Web开发中。(费了半天劲,终于扯入了正题)

Web系统中,Cache的应用点很多,一般来说,Cache被安排在数据瓶颈点的前方,并以这个瓶颈点作为数据源。这也暗合了第一条性质。

比如,很多时候,数据库成为了系统瓶颈,因为它没有那么高的处理能力,可以让应用程序随时、实时地取得数据,所以我们需要Cache,将应用程序中需要数据读取的部分和数据库连接起来。根据Cache的定义描述,我们总是希望程序可以从Cache中直接读取到所需的数据,而不用去查询数据库。

再如,使用一块专用的内存作为磁盘的Cache,程序可以访问这块速度比较高的内存,来代替直接访问相对低速的磁盘设备,以提高整体的IO性能。

Buffer
Buffer其实起初也是一种硬件概念。它指一组固化在存储设备或数据传输设备上的一种特殊内存类元件。比如硬盘上的Buffer,路由器上的Buffer等。后来概念被更广泛地扩展到了软件结构中。八九十年代的玩家大概还记得DOS配置参数(CONFIG.SYS & AUTOEXEC.BAT)里的那句经典得很多人不知道是什么意思的“BUFFER = 9”。

Buffer和Cache有些地方是很像的,但是区别更明显:

首先,Buffer一般大小都是固定的,而且是整块的。也就是说Buffer中通常只有一路单一的数据,而不像Cache可以被很多应用共用(如果不这样,Windows就不能用了)。举例来说,一个socket程序中,接收方开启了一个8192字节的buffer,等待发送方的数据到达,数据是一个bit一个bit地通过网络流进buffer中去的(协议会把数据隐式地重组成字节码),等接收到8192个字节后,buffer满了,程序就把这8192个字节一次从buffer中取出,并把buffer清空,或者宣布它里面的数据无效。这个时候,这8192字节的空间只能做被这个程序做为接收特定地点流入的数据使用。如果另外有一个程序或同一个程序中的另外一段代码使用这块空间做别的事,那么接收程序就会出错,因为它总是整块地把buffer取出来。

另外,Buffer中的数据通常是无法预测的。不像Cache中的数据来自一个已知的数据源,Buffer一般都不能预测其中会接收什么。比如上例中的socket接收程序,很显然,发送方发什么,buffer就会接收什么,在接收到数据之前,接收程序不知道发送方会发些什么东西过来(这有点和写Cache类似)。

与Cache不同,Buffer中的数据通常都是一次性的。数据被取得一次之后就全部失效,而且Buffer的命中率必然100%,因为如果Buffer失效,我们无法再从数据源获取数据。

那么Buffer为什么被叫做“缓冲”呢,它有什么用呢?我们来使用一个实例来说明:

我们写几个简单的小程序,来完成同样的一个功能——复制文件,其中一个不使用Buffer,而另几个使用Buffer。为了描述得更底层,我们使用原始的非流式IO函数。(测试平台:双P3-550,1G ECC,10000RPM SCSI,RHEL3,2.4.1内核)
程序一:prog1.c

#include <unistd.h>
#include <sys/stat.h>
#include <fcntl.h>

int main()
{
char c;
int fd_src, fd_des;
fd_src = open("test.mp3", O_RDONLY);
fd_des = open("test1.mp3", O_WRONLY | O_CREAT, S_IRUSR | S_IWUSR);
while (1 == read(fd_src, &c, 1))
write(fd_des, &c, 1);
exit(0);
}

复制代码
这里我们不使用Buffer,或者说只使用了1个字节的Buffer。这个程序很简单,它复制一个大小为4632576字节的MP3文件——test.mp3,复制到test1.mp3。我们在这个平台上编译执行这个小程序,看看它运行了多长时间:

time ./prog1

real 1m54.259s
user 0m5.000s
sys 1m48.790s

结果表明,这个程序花了近两分钟的时间复制一个正常大小的MP3文件,为什么会出现这种情况呢?因为这个程序每次从test1.mp3中读一个字节,立刻把这个字节写入test1.mp3,源文件有4632576个字节,这个程序就执行了4632576次read,4632576次write,一共执行了9265152次函数调用,即有9265152次函数开销,这简直是难以容忍的!

下面的程序,我们改造一下,使用一个8KB大小的Buffer。
程序二:prog2.c

#include <unistd.h>
#include <sys/stat.h>
#include <fcntl.h>

#define BUFF_SIZE 8192

int main()
{
char buff[BUFF_SIZE];
int fd_src, fd_des;
int nread;

fd_src = open("test.mp3", O_RDONLY);
fd_des = open("test1.mp3", O_WRONLY | O_CREAT, S_IRUSR | S_IWUSR);
while ((nread = read(fd_src, buff, sizeof(buff))) > 0)
write(fd_des, buff, nread);
exit(0);
}

复制代码
除了Buffer大小之外,程序二和程序一几乎完全一样。那么它的测试结果怎样呢:

time ./prog2

real 0m0.110s
user 0m0.000s
sys 0m0.110s

我们得到了一个乍舌的结果:同样的一个mp3文件,复制只花了十分之一秒。那么是为什么呢?因为这个程序中,复制这个4632576字节的文件,只执行了1132次读写操作。

由此我们对于Buffer的作用给出了一个重要的概念:

Buffer的实现目标是为了在一定范围内有效地减少IO调用次数

这和Cache的实现目标有着截然的差别——Cache是为了提高IO的效率。那么为什么说是在一定范围内呢?我们来看看程序三,尽量地开大Buffer,大到可以一次性把文件装入,这样IO调用次数只有2次。我们修改程序三,把BUFF_SIZE改成5MB看看:
程序三:prog3.c

#include <unistd.h>
#include <sys/stat.h>
#include <fcntl.h>

#define BUFF_SIZE 5 * 1024 * 1024

int main()
{
char buff[BUFF_SIZE];
int fd_src, fd_des;
int nread;

fd_src = open("test.mp3", O_RDONLY);
fd_des = open("test1.mp3", O_WRONLY | O_CREAT, S_IRUSR | S_IWUSR);
while ((nread = read(fd_src, buff, sizeof(buff))) > 0)
write(fd_des, buff, nread);
exit(0);
}

复制代码
在开了5MB的Buffer之后,这个程序的执行时间变为:

time ./prog3

real 0m0.133s
user 0m0.000s
sys 0m0.130s

我们发现,它居然比只有8KB缓冲的时候慢了一些,这是为什么呢?我们再贪心一点,打开128MB的Buffer(Am I crazy?)。
程序四:prog4.c

#include <unistd.h>
#include <sys/stat.h>
#include <fcntl.h>

#define BUFF_SIZE 128 * 1024 * 1024

int main()
{
char *buff;
int fd_src, fd_des;
int nread;

buff = (char *) malloc(BUFF_SIZE);

fd_src = open("test.mp3", O_RDONLY);
fd_des = open("test1.mp3", O_WRONLY | O_CREAT, S_IRUSR | S_IWUSR);
while ((nread = read(fd_src, buff, sizeof(buff))) > 0)
write(fd_des, buff, nread);
exit(0);
}

复制代码
程序四直接malloc了128MB的内存做Buffer来用,按照道理来说,128MB和5MB应该都是差不多的,都是一次就可以把文件装入,那么它的结果:

time ./prog4

real 0m29.312s
user 0m1.220s
sys 0m27.940s

结果让我们大跌眼镜,这个程序执行了近半分钟!这是为什么呢?不是说只做了两次IO动作么?

的确是只有2次IO调用,但是这个malloc占用了非常多的时间,因为malloc一块空间需要操作系统来提供一个足够大的连续空闲空间,同时给出一个首地址。这个地址一定要是段首。直接申请大的内存空间是非常耗时的,而且经常失败,因为操作系统要做很多碎片整理工作以保证有这样的一块连续空闲空间给我们使用。同时,大的内存段在IO操作中速度也会变慢。

所以,我们在选择Buffer大小的时候要权衡时空最优比,不要走入极端。





应用
Cache
Cache和Buffer在Web开发中有着非常广的应用。前面已经介绍过了,Cache一般都是为了提高IO效率而加入系统的,其中又包括本地Cache和分布式Cache。有一些成熟的Cache类产品,可以很好地完成我们需要的工作,下面结合BSM中的Cache Module代码来描述一个Cache的应用实例。

下面这个文件,是Cache Module的抽象层,为了兼容PHP4,没有使用接口:
[php]
<?php

/**
*
* @package cache
* @version $Id: cacheal.php,v 1.0.0 2006/10/14 Dr.NP Exp $
* @copyright (c) 2006 BS.Group
* @author Dr.NP <[email protected]>
* @license http://opensource.org/licenses/gpl-license.php GNU Public License
*
*/

/**
* Cache handle
* Abstraction Layer
* @package cache
*/
class cacheal
{
var $classname = 'cacheal';

/**
* Cache lifetime
*/
var $cache_lifetime;

/**
* Config parameters
*/
var $config = array();

/**
* Constructor
*/
function cacheal()
{
// Ignored;
}

/**
* Set cache life time
* Cache will expired if lite time arrived
*/
function set_lifetime($sec = 0)
{
$sec = intval($sec);

if ($sec > 0)
{
$this->cache_lifetime = $sec;
}

return FUNC_RTN_SUCC;
}

/**
* Get value from cache ports by given key
*/
function get($key)
{
return $this->_get($key);
}

/**
* Write value init cache
*/
function set($key, $value)
{
return $this->_set($key, $value);
}

/**
* delete a cache
*/
function del($key)
{
return $this->_del($key);
}
}

?>
[/php]
在这个Class中,定义了对Cache的三种操作——读、写、删除,另外还提供了一个方法用于设置过期时间。遵照BSM的Module标准,在这个al下面会继承若干的驱动文件,可以把需要的Cache类型写成一个Class装进去。

文件:
文本文件是一种最基础的Cache方式。由于系统读写小的文本文件效率要高于直接访问数据库(因为Select的IO操作多于直接访问文件,并且需要一个比较烦琐的过程解析SQL语句),所以用文件来缓存数据库查询结果等是很常见。文件的驱动类如下:
[php]
<?php

/**
*
* @package cache
* @version $Id: cache.file.mod.php,v 1.0.0 2006/10/14 23:16:44 Dr.NP Exp $
* @copyright (c) 2006 BS.Group
* @author Dr.NP <[email protected]>
* @license http://opensource.org/licenses/gpl-license.php GNU Public License
*
*/

// We need the abstract layer.
require_once($module_root . 'cache/cacheal.' . $phpEx);

/**
* Cache file port
* @since 2006-10-14
* @package cache
*/
class BsmCacheFile extends cacheal
{
var $classname = 'BsmCacheFile';

/**
* Cache filename
*/
var $cache_filename;

/**
* Constructor
*/
function BsmCacheFile($config = array())
{
// Ignored.
}

/**
* Get data from cache
*/
function _get($key)
{
$cache_filename = $this->_get_cache_filename($key);

if (!file_exists($cache_filename) || (0 == ($fl = @filesize($cache_filename))))
{
$content = '';
}

// Expire?
else if ($this->cache_lifetime > 0)
{
if (filemtime($cache_filename) + $this->cache_lifetime < time())
{
$content = '';
@unlink($cache_filename);
}
}

else
{
// Ignored.
}

$content = read_file($cache_filename);

return $content;
}

/**
* Save data to cache
*/
function _set($key, $content)
{
$cache_filename = $this->_get_cache_filename($key);

write_file($cache_filename, $content);

return FUNC_RTN_SUCC;
}

/**
* Delete cache
*/
function _del($key)
{
$cache_filename = $this->_get_cache_filename($key);

return @unlink($cache_filename);
}

/**
* Init
*/
function _init()
{
return FUNC_RTN_SUCC;
}

/**
* Calculate cache filename
*/
function _get_cache_filename($key)
{
global $cache_root;

$hash_value = sha1(trim($key));
$cache_dir = $cache_root . substr($hash_value, 0, 2) . '/' . substr($hash_value, 2, 2) . '/';
check_dir($cache_dir);

$cache_filename = $cache_dir . 'cache_' . $hash_value;
return $cache_filename;
}
}

/**
* Current class's name.
*/
$classname = 'BsmCacheFile';

?>
[/php]
都看懂了吧?这个Class很简单,它只是把数据序列化成字符串,写到文本文件里去了。不过它的问题是,sha1存在碰撞,其实这个很容易就可以解决的,各位大侠有兴趣自己来改吧。

值得注意的是,在每次Get操作时,如果设置了过期时间,都要检查Cache文件的创建时间以确定是否过期,如果过期就当作Cache未命中,同时把这个文件真正删除。

这样就引发另一个问题,因为文件是保存在磁盘上,而空间通常只有物理限制。如果一个Cache在被创建后就无人访问,那么这个文件就会永久地存在,时间长了,如果这样“一次性”Cache太多,就会造成很多空间浪费,和大量的磁盘碎片(因为这些文件数量很大,但是大小一般不大)。解决方法也不是很复杂,一般是有一个守护进程或定时程序每过一段时间回收一次空间,各位大侠……那个……呵呵。

APC:
APC是PHP自身支持的一种本地Cache,全名Alternative PHP Cache。它是基于本地内存(或MMAP)的一种实现方式。一方面,它可以用于PHP程序的加速,另一方面它可以按照Key=>Value的方式保存数据,而这正是我们需要的:
[php]
<?php

/**
*
* @package cache
* @version $Id: cache.apc.mod.php,v 1.0.0 2006/10/22 18:15:57 Dr.NP Exp $
* @copyright (c) 2006 BS.Group
* @author Dr.NP <[email protected]>
* @license http://opensource.org/licenses/gpl-license.php GNU Public License
*
*/

// We need the abstract layer.
require_once($module_root . 'cache/cacheal.' . $phpEx);

/**
* cache APC port.
* APC extension needed!
* @since 2006-10-22
* @package
*/
class BsmCacheApc extends cacheal
{
var $classname = 'BsmCacheApc';

/**
* Flag that if the APC extension supported
*/
var $extension_supported = true;
/**
* Constructor
* Check if APC extension loaded
*/
function BsmCache($config = array()){
if (!extension_loaded('apc'))
{
$this->extension_supported = false;
return FUNC_RTN_FAIL;
}
}

/**
* Init
* Ignored.
*/
function _init()
{
return FUNC_RTN_SUCC;
}

/**
* Get data from cache
*/
function _get($key)
{
return ($this->extension_supported) ? apc_fetch($key) : FUNC_RTN_FAIL;
}

/**
* Save data to cache
*/
function _set($key, $value)
{
return ($this->extension_supported) ? apc_store($key, $value, $this->cache_lifetime) : FUNC_RTN_FAIL;
}

/**
* Delete cache
*/
function _del($key)
{
return ($this->extension_supported) ? apc_delete($key) : FUNC_RTN_FAIL;
}
}

/**
* Current class's name.
*/
$classname = 'BsmCacheApc';

?>
[/php]
看看,APC作为本地Cache来使用是多么方便,它省去了文件方式的很多麻烦,而且……它真的很快!根据我之前的测试结果,APC1000000次读和1000000次写操作使用了3秒多时间,快过memcached(localhost)几十倍(还记得我之前说过本地Cache非常不适合用memcached么)。

SHM:
接下来,是一种更低级的内存实现方式——Shared Memory。由于SHM是BSM内核支持的,所以这个Class直接使用了内核提供的接口:
[php]
<?php

/**
*
* @package cache
* @version $Id: cache.shm.mod.php,v 1.0.0 2006/10/22 22:44:46 Dr.NP Exp $
* @copyright (c) 2006 BS.Group
* @author Dr.NP <[email protected]>
* @license http://opensource.org/licenses/gpl-license.php GNU Public License
*
*/

// We need the abstract layer.
require_once($module_root . 'cache/cacheal.' . $phpEx);

/**
* Cache shm port
* Based on shared memory kernel class <shm.inc.php>
* @since 2006-10-22
* @package
*/
class BsmCacheShm extends cacheal
{
var $classname = 'BsmCacheShm';

/**
* Shm object
*/
var $shm_obj;

/**
* Constructor
*/
function BsmCacheShm($config = array())
{
if (!class_exists('BsmShm'))
{
global $include_root, $phpEx;
@require_once($include_root . 'kernel/shm.inc.' . $phpEx);
}

$this->_init();
return class_exists('BsmShm');
}

/**
* Init shared memory object
*/
function _init()
{
if (defined('SHM_SUPPORT'))
{
global $shm;
$this->shm_obj = $shm;
}

else
{
$shm = new BsmShm;
if ($shm->shm_id)
{
$this->shm_obj = $shm;
}

else
{
return FUNC_RTN_FAIL;
}
}

return FUNC_RTN_SUCC;
}

/**
* Get data from cache through shm
*/
function _get($key)
{
return $this->shm_obj->get($this->_make_key($key));
}

/**
* Put data into cache
*/
function _set($key, $value)
{
return $this->shm_obj->put($this->_make_key($key), $value);
}

/**
* Delete cache
*/
function _del($key)
{
return $this->shm_obj->del($this->_make_key($key));
}

/**
* Make a validate key
*/
function _make_key($key)
{
return sha1($key);
}
}

/**
* Current class's name.
*/
$classname = 'BsmCacheShm';

?>
[/php]
和APC类似,共享内存也是基于本地的内存Cache,所不同的是共享内存不支持MMAP,也不会对PHP程序执行产生什么影响,它的实现方式更底层且直接,速度和APC相比差不多,因为两个东西的物理介质和实现过程是一样的。

上面三种,都是本地Cache,下面的就是天杀的Memcached了。这个著名的,高效的分布式内存数据管理器被无数人滥用,以之作为万能缓存。Memcached的内涵很深,它里面处处都蕴涵着经典,更多细节可以参考本人2月初文章。

Memcached
这个Class里,我鬼使神差地使用了一个非官方的Memcached操作扩展,反正都是那么回事,各位大侠……那个:
[php]
<?php

/**
*
* @package cache
* @version $Id: cache.memcached.mod.php,v 1.0.1 2007/01/18 22:05:30 Dr.NP Exp $
* @copyright (c) 2006 BS.Group
* @author Dr.NP <[email protected]>
* @license http://opensource.org/licenses/gpl-license.php GNU Public License
*
*/

// We need the abstract layer.
require_once($module_root . 'cache/cacheal.' . $phpEx);

/**
* Cache memcached port.
* This port only support mcache extension, not for the official PECL extension.
* I wrote a new extension named "Bsm_Memcache" with APR_memcache, but it's not strong enough now
* I think I will put it into my project next month maybe.
* @since 2006-10-22
* @package
*/
class BsmCacheMemcached extends cacheal
{
var $classname = 'BsmCacheMemcached';

/**
* Whether mcache extension supported
*/
var $mcache_supported = true;
/**
* Memcached server(s) info
* Item[host] for host address and [port] for TCP port.
*/
var $servers = array();

/**
* Mcache handle
*/
var $mc;

/**
* Constructot
* Check if mcache extension loaded
*/
function BsmCacheMemcached($config = array)
{
if (!extension_loaded('mcache'))
{
$this->mcache_supported = false;
return FUNC_RTN_FAIL;
}

$this->config = $config;
return $this->_init($config);
}

/**
* Init
* Set memcached parameters
*/
function _init($config)
{
if (!$this->mcache_supported == true)
{
return FUNC_RTN_FAIL;
}

// Init memcached handle
// I am sure, not "new memcache()"
@$mc = memcache();
if (!$mc)
{
return FUNC_RTN_FAIL;
}

$this->mc = $mc;

// Add servers
$this->servers = $config['servers'];
if (!is_array($this->servers))
{
$this->servers = array();
}

foreach($this->servers as $mc_server)
{
if (is_validate_ip($mc_server['host']) && is_validate_port($mc_server['port']))
{
$this->mc->add_server($mc_server['host'], $mc_server['port']);
}
}
}

/**
* Get data from cache
*/
function _get($key)
{
return ($this->mcache_supported) ? $this->mc->get($key) : FUNC_RTN_FAIL;
}

/**
* Save data to cache
*/
function _set($key, $value)
{
return ($this->mcache_supported) ? $this->mc->set($key, $value, $this->cache_lifetime) : FUNC_RTN_FAIL;
}

/**
* Delete cache
*/
function _del($key)
{
return ($this->mcache_supported) ? $this->mc->delete($key) : FUNC_RTN_FAIL;
}
}

/**
* Current class's name.
*/
$classname = 'BsmCacheMemcached';

?>
[/php]
总有一些事,是被我们忘怀的,也总有一些事,让我们怀恨在心……APC有一个apc_cache_info()函数,来显示Cache详细信息,而Memcached一如既往地无法获取列表,当然这和它的数据组织方式有很大关系,想要列表就得遍历,当然这不是不能实现的,虽然代价有点高。

Memcached是典型的分布式Cache,它可以做到本地Cache无法做到的,那就是——共享。我们为什么要共享Cache?因为很多事,例如Session。在SSO结构中,共享Session似乎是个永恒的话题,而分布式Cache是实现共享的最直接手段。

分析一下以上四种Cache实现方式,同时思考一下在自己的系统中,究竟需要什么样的Cache,什么时候适合使用什么,而不要一味地把Memcached当成万能的。BSM对它们的支持很灵活,可以简单地通过修改load_module的传入参数修改Cache实现方式,然而方便的背后,我们需要清醒地知道,我们究竟在缓存什么。

接下来,是一个关于数据库的实现实例。BSM的db module完全支持cache module。它在支持Cache的系统中会根据操作需要自动将查询结果数据做缓存,如果一组数据被访问多次,那么可以很大程度上降低数据库的压力。

如上,还是使用那种Abstract Layer->Driver的方式(本人受PHPBB毒害很深)。这个al中使用了一些常量,和function包里的一些函数,所以不经修改不能独立使用的:
[php]
<?php

/**
*
* @package db
* @version $Id: dbal.php,v 1.0.0 2006/11/9 23:29:30 Dr.NP Exp $
* @copyright (c) 2006 BS.Group
* @author Dr.NP <[email protected]>
* @license http://opensource.org/licenses/gpl-license.php GNU Public License
*
*/

/**
* Database Handler Abstraction Layer
* @since 2006-11-9
* @package db
*/
class dbal
{
var $classname = 'dbal';

/**
* Current connection identifier
*/
var $db_connection_id;

/**
* Query result
*/
var $query_result;

/**
* Stat of queries
*/
var $num_queries = array();

/**
* Cache flag
*/
var $query_fromcache = false;

/**
* Cache content
*/
var $cache_content = '';

/**
* Cache cursor
*/
var $cache_offset = 0;

/**
* Transaction status
*/
var $sql_transaction = false;

/**
* Connection informations
*/
var $conn_host;
var $conn_port;
var $conn_user;
var $conn_dbname = '';
var $conn_persistency = false;

/**
* Constructor
*/
function dbal()
{
$this->num_queries = array(
'normal' => 0,
'cached' => 0,
'failed' => 0
);
}

/**
* Fetch how many times query
*/
function sql_num_queries()
{
return $this->num_queries;
}

/**
* Base query method
*/
function sql_query($query, $fromcache = false, $cache_key = false)
{
if ('SELECT' != strtoupper(substr($query, 0, 6)))
{
$fromecache = false;
$this->query_fromcache = false;

if (defined('CACHE_LAYER') && $cache_key)
{
global $cache;

$cache->del($cache_key);
}
}

// Cache enabled?
if (defined('CACHE_LAYER') && $fromcache)
{
global $cache;
$query_cache_key = ($cache_key) ? $cache_key : $query;
$content = $cache->get($query_cache_key);

if (false != $content)
{
$this->query_fromcache = true;
$this->num_queries['cached'] += 1;
$this->cache_content = unserialize($content);
return true;
}
}

$ret = $this->_sql_query($query);
$this->query_fromcache = false;
if (defined('CACHE_LAYER') && $fromcache)
{
$cache->set($query_cache_key, serialize($this->sql_fetchrowset()));
}

return $ret;
}

/**
* Fetch rowset
*/
function sql_fetchrow($query_id = false)
{
// From cache
if (defined('CACHE_LAYER') && $this->query_fromcache)
{
$result = $this->cache_content[$this->cache_offset];
$this->cache_offset++;

if ($result)
{
return $result;
}
}

return $this->_sql_fetchrow($query_id);
}

/**
* Get all rows by query
*/
function sql_fetchrowset($query_id = false)
{
if (defined('CACHE_LAYER') && $this->query_fromcache)
{
$result = $this->cache_content;

if (is_array($result))
{
return $result;
}
}

if (!$query_id)
{
$query_id = $this->query_result;
}

if ($query_id)
{
$this->sql_rowseek(0);
$result = array();
while ($row = $this->_sql_fetchrow($query_id))
{
$result[] = $row;
}

return $result;
}

return false;
}

/**
* Get one element by query
*/
function sql_fetchone($query_id = false)
{
if (!$query_id)
{
$query_id = $this->query_result;
}

if ($query_id)
{
$row = $this->sql_fetchrow($query_id);
if (is_array($row))
{
$result = array_shift($row);
unset($row);
return $result;
}
}

return false;
}

/**
* Close SQL Connection
*/
function sql_close()
{
if (!$this->db_connection_id && !$this->master_db_connection_id && !$this->slave_db_connection_id)
{
return false;
}

if ($this->transaction)
{
$this->sql_transaction('commit');
}

return $this->_sql_close();
}

/**
* SQL Transaction
*/
function sql_transaction($status = 'begin')
{
switch ($status)
{
case 'begin':
// Commit previously opened transaction before opening another transaction
if ($this->transaction)
{
$this->_sql_transaction('commit');
}

$result = $this->_sql_transaction('begin');
$this->transaction = true;
break;

case 'commit':
$result = $this->_sql_transaction('commit');
$this->transaction = false;

if (!$result)
{
$this->_sql_transaction('rollback');
}
break;

case 'rollback':
$result = $this->_sql_transaction('rollback');
$this->transaction = false;
break;

default:
$result = $this->_sql_transaction($status);
break;
}

return $result;
}
}

?>
[/php]
有点点特殊,就是如果要使用Cache,在sql_query的时候要传入一个key作为标记。开始的时候打算直接用SQL来生成,后来涉及到更新问题。通过自定义标记,就可以在UPDATE、DELETE、INSERT的同时把对应的Cache清除,这样SELECT到的数据就是实时的了。注意这个db module是支持M/S主从结构的,在下面的MySQL驱动中,可以很清晰地体现:
[php]
<?php

/**
*
* @package db
* @version $Id: db.mysql.mod.php,v 1.0.0 2006/11/9 23:58:36 Dr.NP Exp $
* @copyright (c) 2006 BS.Group
* @author Dr.NP <[email protected]>
* @license http://opensource.org/licenses/gpl-license.php GNU Public License
*
*/

// Abstraction layer needed.
require_once($module_root . 'db/dbal.' . $phpEx);

/**
* Database Handler: Mysql
* @since 2006-11-9
* @package db
*/

class BsmDbMysql extends dbal
{
var $classname = 'BsmDbMysql';

/**
* Set this for enable MySQL Master/Slave bin-log backup mode
*/
var $is_master_slave = false;

/**
* For Master/Slave below:
*/
var $master_conn_host;
var $master_conn_user;
var $master_conn_dbname;
var $master_db_connection_id;

var $slave_conn_host;
var $slave_conn_user;
var $slave_conn_dbname;
var $slave_db_connection_id;

var $srv_flag;

/**
* Constructor
*/
function BsmDbMysql($is_master_slave = false)
{
$this->is_master_slave = ($is_master_slave) ? true : false;
return $this->dbal();
}

/**
* Connect to the database
*/
function sql_connect($db_host, $db_port, $db_user, $db_pass, $db_name, $persistency, $is_master = false)
{
$this->persistency = $persistency;
$db_host = $db_host . (($db_port) ? ':' . $db_port : '');

if (!$this->is_master_slave)
{
$this->conn_host = $db_host;
$this->conn_user = $db_user;
$this->conn_dbname = $db_name;
}

else
{
$srv_flag = ($is_master) ? 'master' : 'slave';

if ($is_master)
{
$this->master_conn_host = $db_host;
$this->master_conn_user = $db_user;
$this->master_conn_dbname = $db_name;
}

else
{
$this->slave_conn_host = $db_host;
$this->slave_conn_user = $db_user;
$this->slave_conn_dbname = $db_name;
}
}

if ($this->persistency)
{
$r = @mysql_pconnect($db_host, $db_user, $db_pass);
}

else
{
$r = @mysql_connect($db_host, $db_user, $db_pass);
}

if ($r && @mysql_select_db($db_name, $r))
{
if (!$this->is_master_slave)
{
$this->db_connection_id = $r;
}

else
{
if ($is_master)
{
$this->master_db_connection_id = $r;
}

else
{
$this->slave_db_connection_id = $r;
}

$this->srv_flag = $srv_flag;
}

// UTF-8 here...
@mysql_query("SET NAMES utf8", $r);
return $r;
}

return $this->sql_error();
}

/**
* Return number of rows
*/
function sql_numrows($query_id = false)
{
if (!$query_id)
{
$query_id = $this->query_result;
}

return ($query_id) ? @mysql_num_rows($query_id) : false;
}

/**
* Free SQL result
*/
function sql_freeresult($query_id = false)
{
if (!$query_id)
{
$query_id = $this->query_result;
}

return ($query_id) ? @mysql_free_result($query_id) : false;
}

/**
* Get last inserted id after insert statement
*/
function sql_nextid()
{
return ($this->db_connect_id) ? @mysql_insert_id($this->db_connect_id) : false;
}

/**
* Seek to given row number
* rownum is zero-based
*/
function sql_rowseek($rownum, $query_id = false)
{
if (!$query_id)
{
$query_id = $this->query_result;
}

if ($this->query_fromcache && $this->cache_offset > 0)
{
$this->cache_offset = $rownum;
}

return ($query_id) ? @mysql_data_seek($query_id, $rownum) : false;
}

/**
* Escape string used in sql query
*/
function sql_escape($msg)
{
if (!$this->db_connect_id)
{
return @mysql_real_escape_string($msg);
}

return @mysql_real_escape_string($msg, $this->db_connect_id);
}

/**
* Output SQL error message
*/
function sql_error($info = '')
{
die ('<b>Database error: </b>' . @mysql_error() . (($info) ? ' - ' . $info : ''));
}

/**
* Query from database
*/
function _sql_query($query = '')
{
$query = trim($query);

if ($query == '')
{
return false;
}

if ($this->is_master_slave)
{
if ('SELECT' == strtoupper(substr($query, 0, 6)))
{
// Slave selected.
$this->db_connection_id = $this->slave_db_connection_id;
}

else
{
$this->db_connection_id = $this->master_db_connection_id;
}
}

if (($this->query_result = @mysql_query($query, $this->db_connection_id)) === false)
{
$this->sql_error($query);
}

if ($this->query_result)
{
// No cache controller now.
$this->num_queries['normal'] += 1;
}

else
{
$this->num_queries['failed'] += 1;
}

return ($this->query_result) ? $this->query_result : false;
}

/**
* Fetch current row
*/
function _sql_fetchrow($query_id = false, $contain_num = false)
{
if (!$query_id)
{
$query_id = $this->query_result;
}

if ($query_id)
{
if ($contain_num)
{
return @mysql_fetch_array($query_id, MYSQL_BOTH);
}

else
{
return @mysql_fetch_assoc($query_id);
}
}

return false;
}

/**
* SQL Transaction
*/
function _sql_transaction($status = 'begin')
{
switch ($status)
{
case 'begin':
return @mysql_query('BEGIN', $this->db_connect_id);
break;

case 'commit':
return @mysql_query('COMMIT', $this->db_connect_id);
break;

case 'rollback':
return @mysql_query('ROLLBACK', $this->db_connect_id);
break;
}

return true;
}

/**
* Close Connection
*/
function _sql_close()
{
if ($this->is_master_slave)
{
$this->db_connection_id = null;
@mysql_close($this->master_db_connection_id);
@mysql_close($this->slave_db_connection_id);
return;
}

else
{
return @mysql_close($this->db_connection_id);
}
}
}

/**
* Current class name
*/
$classname = 'BsmDbMysql';

?>
[/php]
在_sql_query方法中,如果工作在主从模式下,程序会根据SQL是否是SELECT来决定使用主库还是从库。

在db module中,也是有一些问题的。比如在使用多台服务器时,一台服务器更新了Cache,另外的服务器会不会得到实时数据?当然使用分布式Cache(如Memcached)是没问题,那么使用APC一类的本地Cache呢?各位大侠……呵呵……呵呵……
那么在使用这个module时,只要简单地:

$db->sql_query(“SELECT info FROM table LIMIT 10”, true, ‘my_cache’);

复制代码
就OK了。第一次它会真正地查询数据库,之后如果这个数据表没有变化,它会从Cache中直接取道结果。sql_num_queries方法可以返回一个数组,记录从程序开始,有多少次真实查询,有多少次从Cache中获取,有多少次查询失败。
另外,这种方式另有不足,就是当查询结果级很大的时候,是不能使用Cache的,所以sql_query方法并不强制使用,而留了一个boolean的开关。

那么在其它环境中呢,Cache可能使用得比较直接,如页面缓存等。其实某种意义上来说,模板编译引擎的编译结果也算是一种Cache,只不过现在大多数引擎使用的都是文件方式,其实如果使用APC把这些编译好的可执行文件加载到内存区,可以获得更好的IO性能和执行性能。

APC具体参数可以参考手册。要注意的一点是ini文件配置时,不要把APC使用的SHM块设置得过大,可以使用多个小块。因为不是所有的UNIX类系统都支持32MB以上的共享内存区。

Buffer
Buffer的应用比较不好描述,因为它使用得比Cache要隐讳。其实Buffer无处不在,在很多系统提供的函数中都可以看到Buffer存在。

首先值得注意的,是PHP中相对应C的低级IO的DirectIO函数组。例如dio_read()函数,它的原型为:

string dio_read ( resource fd [, int len] )

后面那个len,表示要读多少个字节。如果不指定,就会读1KB。实际上,这个就是Buffer长度。结合我们之前做的那几个复制MP3文件的小程序来看,这个Buffer也起到和字符串变量c相同的作用。但是在dio_write()中:

int dio_write ( resource fd, string data [, int len] )

这个len就不是Buffer长度了,它是指需要写入的数据长,如果不指定,函数会把data完全写入,并返回实际写入的长度。通常情况下,函数返回值和len是相等的,当len大于data的实际长度时,就会出现函数返回值小于len的情况,这个一般可以用于检测数据末尾,在复制MP3的程序中就使用了这种方法。

接下来,注意一下socket函数组。其中socket_read:

string socket_read ( resource socket, int length [, int type] )

和dio_read非常像(其实Socket无非也是一种IO),它使用length参数来标记Buffer长度,只不过这个参数是必须指定的,没有默认值。

socket_recv和socket_recvfrom这两个函数赤裸裸地(-_-!)提出了Buffer的概念:

int socket_recv ( resource socket, string &buf, int len, int flags )
int socket_recvfrom ( resource socket, string &buf, int len, int flags, string &name [, int &port] )

系统函数中使用Buffer的例子非常多,细心的大侠可以去翻翻手册看。

那么在我们的设计中,Buffer都做了什么呢?举个小例子,我们可以考虑下面一个问题:模拟Linux的tail命令,即显示文件的最后N行。这个很有用,例如查看Log文件的最新10条。写这样的一个小函数:
[php]
function _tail($n)
{
$t_size = 4096 * $n;

$log_file_name = $this->_log_file();

if (!@$fp = fopen($log_file_name, 'rb'))
{
return FUNC_RTN_FAIL;
}

flock($fp, LOCK_SH);

fseek($fp, filesize($log_file_name) - $t_size);
$content = fread($fp, $t_size);
fclose($fp);
$lines = explode($this->crlf, trim($content));
$ret = array();

for ($i = 0; $i < $n; $i++)
{
$line = array_pop($lines);
if ($line)
{
$ret[] = trim($line);
}
}

$ret = array_reverse($ret);

return $ret;
}
[/php]
这是log module的文件实现类里的一个方法。这里使用了一个经验值4096,即我们认为每行日志的长度都不会大于4096个字节,这样$t_size就是最后n行的可能的最大长度。fread读入了文件的最后$t_size字节,写入$content,其实这个$content就是Buffer,它的长度是$t_size。之后再整理Buffer中的数据,按行来分割,取到最后n行来。

考虑一下,如果无法做4096字节的限制,那么一次读入的数据可能不够n行,那么这个Buffer($content)就要被清空,再次读取上面的$t_size字节的数据,直到满足了取到n行,或者到达文件头(不是文件尾,因为是倒着读的,所以不要feof)。具体实现方法,各位大侠……呵呵……呵呵……呵呵……

那么如果我们不使用Buffer,结果会怎样?很容易想象到,这个函数会疯狂地fseek,和之前复制MP3文件的程序一样,这样的函数调用开销会非常大。假设最后100行有1MB个字节,那么_tail(100)就会调用1048576次fseek,这个结果将是相当恐怖的。

结案
《大型》系列为什么被叫做“入门”,而不是“进阶”、“提高”呢?因为我的主要目的是向读者阐述“这个东西是什么”,“它在做什么”,“这样做有什么好处”。在涉及大型架构时,需要知道更多的“为什么”。

本篇意在解释Cache和Buffer“是什么”,它们都“做什么”,它们“怎么用”。不免会有一些比较晦涩的东西看似不属于“入门”,其实仔细思考一下,这些东西对提高还是很有好处的。我也留下了很多“各位大侠……呵呵……”,不是我欠打,我是故意的(-_-!)。

我们总结了Cache的几条规律,和Buffer的若干性质,希望大家在架构设计中认清,在什么样的情况下使用什么样的解决方案,可以用最小的成本换来最大的收益,这才是大型应用的核心思想。祝大家走得更好更远。

后记
本来想画很多图的……不过我太懒了,下次再说吧,哈哈……哈哈……哈哈……哈哈……哈哈……(我是不是很欠打)

09/08/2007
NP博士
 

你可能感兴趣的:(PHP)