2009年7月15日星期三

Linux中XFS文件系统的使用及创建方法


Linux中XFS文件系统的使用及创建方法


XfS文件系统是SGI开发的高级日志文件系统,XFS极具伸缩性,非常健壮。所幸的是SGI将其移植到了Linux系统中。在Linux环境下。目前版本可用的最新XFS文件系统的为1.2版本,可以很好地工作在2.4核心下。

一、XFS文件系统简介

主要特性包括以下几点:

数据完全性

采用XFS文件系统,当意想不到的宕机发生后,首先,由于文件系统开启了日志功能,所以你磁盘上的文件不再会意外宕机而遭到破坏了。不论目前文件系统上存储的文件与数据有多少,文件系统都可以根据所记录的日志在很短的时间内迅速恢复磁盘文件内容。

传输特性

XFS文件系统采用优化算法,日志记录对整体文件操作影响非常小。XFS查询与分配存储空间非常快。XFS文件系统能连续提供快速的反应时间。笔者曾经对XFS、JFS、Ext3、ReiserFS文件系统进行过测试,XFS文件文件系统的性能表现相当出众。

可扩展性

XFS 是一个全64-bit的文件系统,它可以支持上百万T字节的存储空间。对特大文件及小尺寸文件的支持都表现出众,支持特大数量的目录。最大可支持的文件大小为263 = 9 x 1018 = 9 exabytes,最大文件系统尺寸为18 exabytes。

XFS使用高的表结构(B+树),保证了文件系统可以快速搜索与快速空间分配。XFS能够持续提供高速操作,文件系统的性能不受目录中目录及文件数量的限制。

传输带宽

XFS 能以接近裸设备I/O的性能存储数据。在单个文件系统的测试中,其吞吐量最高可达7GB每秒,对单个文件的读写操作,其吞吐量可达4GB每秒。

二、XFS文件系统的使用

1.下载与编译内核

下载相应版本的内核补丁,解压补丁软件包,对系统核心打补丁。

下载地址:ftp://oss.sgi.com/projects/xfs/d ... .4.18-all.patch.bz2

对核心打补丁,下载解压后,得到一个文件:xfs-1.1-2.4.18-all.patch文件。

对核心进行修补如下:






# cd /usr/src/linux 
# patch -p1 < /path/to/xfs-1.1-2.4.18-all.patch

修补工作完成后,下一步要进行的工作是编译核心,将XFS编译进Linux核心可中。

首先运行以下命令,选择核心支持XFS文件系统:

#make menuconfig

在“文件系统“菜单中选择:

<*> SGI XFS filesystem support ##说明:将XFS文件系统的支持编译进核心或 SGI XFS。 filesystem support ##说明:以动态加载模块的方式支持XFS文件系统。

另外还有两个选择:

Enable XFS DMAPI ##说明:对磁盘管理的API,存储管理应用程序使用。

Enable XFS Quota ##说明:支持配合Quota对用户使用磁盘空间大小管理。

完成以上工作后,退出并保存核心选择配置。之后,然后编译内核,安装核心:






        #make bzImage 

        #make module

        #make module_install

        #make install

如果你对以上复杂繁琐的工作没有耐心或没有把握,那么可以直接从SGI的站点上下载已经打好补丁的核心,其版本为2.4.18。它是一个rpm软件包,你只要简单地安装即可。SGI提交的核心有两种,分别供smp及单处理器的机器使用。

2.创建XFS文件系统

完成对核心的编译后,还应下载与之配套的XFSprogs工具软件包,也即mkfs.xfs工具。不然我们无法完成对分区的格式化:即无法将一个分区格式化成XFS文件系统的格式。要下载的软件包名称:xfsprogs-2.0.3。

将所下载的XFSProgs工具解压,安装,mkfs.xfs自动安装在/sbin目录下。






        #tar –xvf xfsprogs-2.0.3.src.tar.gz 

        #cd xfsprogs-2.0.3src

        #./configure

        #make

        #make install

使用mkfs.xfs格式化磁盘为xfs文件系统,方法如下:

# /sbin/mkfs.xfs /dev/sda6 #说明:将分区格式化为xfs文件系统,以下为显示内容:






        meta-data=/dev/sda6 isize=256 agcount=8, agsize=128017 blks 

        data = bsize=4096 blocks=1024135, imaxpct=25

        = sunit=0 swidth=0 blks, unwritten=0

        naming =version 2 bsize=4096

        log =internal log bsize=4096 blocks=1200

        realtime =none extsz=65536 blocks=0, rtextents=0

格式化磁盘时,如果mkfs.xfs提示你分区原本已被格式化为其它文件系统,可以使用参数 –f 强行格式化:

#/sbin/mkfs.xfs –f /dev/sda6

3.加载XFS文件系统

#mount –t xfs /dev/sda6 /xfs ##其中/xfs是主分区/下的一个目录。

最后,为了让系统启动后就自动加载,应该更改/etc/fstab,这样系统启动后就会自动加载xfs分区而不必每次都手工加载。

要说明的一点是目前的xfs由于受Linux内存页限制,在x86版本中,只能实现文件系统的块尺寸为4K。另外,XFS文件系统可以不同
的方式 mount,即允许文件系统以读方式加载,也允许以读写方式加载。这是因为xfs文件系统用作根文件系统时,为了安全要以只读方式加载。





diff, patch和quilt

diff, patch和quilt


diff和patch是在Linux环境为源代码制作和应用补丁的标准工具。diff可以比较文件或目录的差异,并将差异记录到补丁文件。
patch可以将补丁文件应用到源代码上。quilt也是一个制作和应用补丁的工具,它适合于管理较多补丁。quilt有自己的特有的工作方式。本文通过
简单的例子介绍这三个常用的工具。


0 示例工程


我们先准备一个用来做实验的工程,它包含若干子目录和文件。可以用find命令列出文件清单:



$ find old-prj/ -type f

old-prj/inc/def1.h

old-prj/inc/def2.h

old-prj/src/sys/sys1.c

old-prj/src/sys/sys1.h

old-prj/src/app/app1.c

old-prj/src/app/app2.c

old-prj/src/app/app2.h

old-prj/src/app/app1.h

old-prj/src/drv/drv1.h

old-prj/src/drv/drv2.c

old-prj/src/drv/drv1.c

old-prj/src/drv/drv2.h

old-prj/build/Makefile


find命令的"-type f"参数选择普通文件,可以省略掉目录。希望自己操作的读者可以下载这个示例工程


1 diff和patch


1.1 比较一个文件


将old-prj.tar.bz2放到我们的工作目录,然后建立一个子目录,进入后解压示例工程:



$ mkdir test1; cd test1; tar xvjf ../old-prj.tar.bz2


用分号分隔多个命令可以节省篇幅。将old-prj复制到new-prj:



$ cp -a old-prj/ new-prj


让我们编辑一个文件。src/drv/drv1.h的内容本来是:



$ cat -n old-prj/src/drv/drv1.h

     1  #ifndef DRV1_H

     2  #define DRV1_H

     3

     4  #include "def1.h"

     5

     6  typedef struct {

     7    int p1;

     8    int p2;

     9    int p3;

    10  } App1;

    11

    12  void do_app1(void);

    13

    14  #endif


cat命令的"-n"参数可以增加行号。我们用vi将它修改成:



$ cat -n new-prj/src/drv/drv1.h

     1  #ifndef DRV1_H

     2  #define DRV1_H

     3

     4  #include "def1.h"

     5

     6  typedef struct {


     7    int a;

     8    int b;


     9  } App1;

    10

    11  void do_app1(void);

    12

    13  #endif


现在可以用diff命令比较文件了:



$ diff -u old-prj/src/drv/drv1.h new-prj/src/drv/drv1.h

--- old-prj/src/drv/drv1.h      2008-03-01 12:59:46.000000000 +0800

+++ new-prj/src/drv/drv1.h      2008-03-01 13:07:14.000000000 +0800

@@ -4,9 +4,8 @@

 #include "def1.h"

 

 typedef struct {

-  int p1;

-  int p2;

-  int p3;

+  int a;

+  int b;

 } App1;

 

 void do_app1(void);


diff程序按行比较文本文件。比较文件的diff命令格式是:



$ diff -u 旧文件 新文件


"-u"参数指定diff命令使用 unified 格式,这是一种最常用的格式,我们来看看它的含义。


1.2 diff的 unified 格式


以"---"开头的行是旧文件信息,以"+++"开头的行是新文件信息:



--- old-prj/src/drv/drv1.h      2008-03-01 12:59:46.000000000 +0800

+++ new-prj/src/drv/drv1.h      2008-03-01 13:07:14.000000000 +0800


unified 格式默认在变化部分的前后各显示三行上下文。在上例中,旧文件的7、8、9行被替换成新文件的7、8行。旧文件的变化部分是7-9行,前后多显示3行,
因此显示4-12行。新文件的变化部分是7-8行,前后多显示3行,因此显示4-11行。以"@@"包围的行指示补丁的范围:



@@ -4,9 +4,8 @@


'-4,9'中,'-'表示旧文件,'4,9'表示从第4行开始,显示9行,即显示4-12行。'+4,8'中,'+'表示新文件,'4,8'表示
从第4行开始,显示8行,即显示4-11行。"@@"行之后是上下文和变化的文本,其中'-'开头的行是旧文件特有的,'+'开头的行是新文件特有的,其
它行是两个文件都有的,即补丁的上下文。例如:



 #include "def1.h"

 

 typedef struct {

-  int p1;

-  int p2;

-  int p3;

+  int a;

+  int b;

 } App1;

 

 void do_app1(void);


1.3 制作和应用补丁


所谓制作补丁就是diff的输出重定向到一个文件,这个文件就是补丁文件。例如:



$ diff -u old-prj/src/drv/drv1.h new-prj/src/drv/drv1.h>../drv1.diff


我们将old-prj解压到另一个目录,准备应用这个补丁:



$ cd ..; mkdir test2; cd test2; tar xvjf ../old-prj.tar.bz2; mv old-prj myprj; cd myprj


在真实场景中,test2目录通常是在用户2的电脑上。用户2可能不使用 old-prj 作为第一级目录的名字。例如:用户1的第一级目录名是 linux-2.6.23.14,
用户2的第一级目录名是linux。所以我们将 old-prj 改为 myprj 以模拟这种情况。


我们在 myprj 目录使用patch命令应用补丁:



$ patch -p1 < ../../drv1.diff

patching file src/drv/drv1.h


patch命令行中为什么没有出现要打补丁的文件?这是因为patch命令可以使用补丁文件中的文件信息:



--- old-prj/src/drv/drv1.h      2008-03-01 12:59:46.000000000 +0800


"-pn"参数(上例中n=1)中的n表示要从补丁文件的文件路径中去掉几层目录,可以理解为去掉几个'/'。例如:p1表示去掉一层目
录,"old-prj/src/drv/drv1.h"去掉一层就成为"src/drv/drv1.h"。patch命令在 myprj
目录找到"src/drv/drv1.h"后应用补丁。


我们通常都在代码树的上一层目录制作补丁,在代码树的根目录应用补丁。因此,最常用的patch命令格式是:



$ patch -p1 < 补丁文件


1.4 比较目录


我们回到test1目录,再对 new_prj 做一些改动。这次我们删除掉src/sys目录及其中的文件。再建立src/usr目录,并在该目录增加两个文件usr1.h和usr1.c。



$ cd ../../test1; rm -rf new-prj/src/sys; mkdir new-prj/src/usr

$ echo -e "#ifndef USR1_H\n#define USR1_H\n#include \"def1.h\"\n#endif">new-prj/src/usr/usr1.h

$ echo -e "#include \"usr1.h\"">new-prj/src/usr/usr1.c


echo命令的"-e"参数打开对转义符的支持,bash默认是不支持转义符的。


现在我们比较目录并制作补丁:



$ diff -Nur old-prj/ new-prj/ > ../prj.diff


读者可以cat这个补丁文件的内容。根据前面的介绍,读者应该能看懂补丁文件了吧。


比较目录的常用命令是:



$ diff -Nur 旧目录 新目录 > 补丁文件




$ diff -Naur 旧目录 新目录 > 补丁文件


"-u"参数前面已经介绍过了。"-N"参数将不存在的文件当作空文件。如果没有这个参数,补丁就不会包含孤儿文件(即另一方没有的文件)。"-r"参数表示比较子目录。"-a"参数表示将所有文件当作文本文件。


我们再准备一个目录来应用补丁:



$ cd ..; mkdir test3; cd test3; tar xvjf ../old-prj.tar.bz2; mv old-prj myprj; cd myprj


在源代码树的根目录应用补丁:



$ patch -p1 < ../../prj.diff

patching file src/drv/drv1.h

patching file src/sys/sys1.c

patching file src/sys/sys1.h

patching file src/usr/usr1.c

patching file src/usr/usr1.h


好了,读者可以用"diff -Nur"比较一下"test1/new_prj"和"test3/myprj",没有输出就表示完全相同。



$ cd ../..; diff -Nur test1/new-prj test3/myprj


1.5 很多的补丁...


一个大项目可能有不同开发者提供很多补丁。这些补丁可能还存在依赖关系,例如补丁B必须打在补丁A上。我们当然可以凭着程序员的“心细如发”去管理好这些补丁,不过有一个叫quilt的工具可以使我们轻松一些。当然,即使有工具的帮助,细心和认真也是必需的。


附录


为了简单起见,前面只介绍了一个"diff -Nur 老目录
新目录"的用法。有时候,新目录里只放了修改过的文件。这时可以不使用-N参数以忽略孤儿文件,即"diff -ur 老目录
新目录"。diff会输出孤儿文件的提示,我们可以删除或保留这些提示,它们对patch没有影响。


使用diff时可以用--exclude排除文件和目录,例如:



diff -ur -exclude=.* --exclude=CVS prj_old prj_new

上例排除了源代码树中以'.'开头的文件和所有CVS目录。其实对于CVS项目,可以直接在源代码树根目录中执行:


cvs diff -u3 > 补丁文件名

u3表示输出3行上下文的unified 格式。打补丁时在源代码树根目录中执行:



patch -p0 < 补丁文件名

"cvs diff"会自动忽略CVS项目外的文件。通过CVS的tag和补丁文件,我们可以方便地保存工作快照。



2 quilt


我们自己的项目可以用cvs或svn管理全部代码。但有时我们要使用其他开发者维护的项目。我们需要修改一些文件,
但又不能直接向版本管理工具提交代码。自己用版本管理工具重建整个项目是不合适的,因为大多数代码都是别人维护的,例如Linux内核。我们只是想管理好
自己的补丁。这时可以使用quilt。


2.1 基本概念


quilt是一个帮助我们管理补丁的程序。quilt的命令格式类似于cvs:


quilt 子命令 [参数]


0.46版的quilt有29个子命令。


掌握quilt的关键是了解使用quilt的流程。使用quilt时,我们会在一个完整的源代码树里工作。只要我们
在源代码树里使用了quilt命令,quilt就会在源代码树的根目录建立两个特殊目录:patches和.pc。quilt在patches目录保存它
管理的所有补丁。quilt用.pc目录保存自己的内部工作状态,用户不需要了解这个目录。


patches/series文件记录了quilt当前管理的补丁。补丁按照加入的顺序排列,早加入的补丁在前。quilt用堆栈的概念管理补丁的应用。



我们在应用补丁A前,必须先应用所有早于补丁A的补丁。所以,patches/series中的补丁总是从上向下应
用。例如:上图中,补丁1到补丁5是已经应用的补丁。我们可以将已应用的补丁想象成一个向下生长的堆栈,栈顶就是已应用的最新补丁。应用补丁就是将补丁入
栈,撤销补丁就是将补丁出栈。


我们在源代码树中作任何修改前,必须用"quilt add"命令将要修改的文件与一个补丁联系起来。在完成修改后,用"quilt refresh"命令将修改保存到已联系的补丁。下面我们通过一篇流程攻略来认识一下quilt的命令。


2.2 导入补丁


我们把 old-prj.tar.bz2 想象成Linux内核,我们把它解压后,进入代码树的根目录:


$ mkdir qtest; cd qtest; tar xvjf ../old-prj.tar.bz2; mv old-prj prj; cd prj


在修改代码前,我们通常要先打上官方补丁。在quilt中,可以用import命令导入补丁:


$ quilt import ../../prj.diff


Importing patch ../../prj.diff (stored as prj.diff)


执行improt命令后, prj 目录会多出一个叫 patches 的子目录:


$ find patches/ -type f


patches/prj.diff


patches/series


quilt在这个目录存放所有补丁和前面介绍的series文件。quilt的大多数命令都可以在代码树的任意子目录运行,不一定要从根目录运行。我们可以用applied命令查询当前已应用的补丁。


$ quilt applied


No patches applied


目前还没有应用任何补丁。unapplied命令查询当前还没有应用的补丁,top命令查询栈顶补丁,即已应用的最新补丁:


$ quilt unapplied


prj.diff


$ quilt top


No patches applied


我们可以使用push命令应用补丁,例如:


$ quilt push -a


Applying patch prj.diff


patching file src/drv/drv1.h


patching file src/sys/sys1.c


patching file src/sys/sys1.h


patching file src/usr/usr1.c


patching file src/usr/usr1.h


Now at patch prj.diff


push的"-a"参数表示应用所有补丁。在使用push命令后,prj 目录会多了一个叫.pc的隐含子目录。quilt用这个目录保存内部状态,用户不需要了解这个目录。应用补丁后,我们再使用applied、unapplied和top命令查看:


$ quilt applied


prj.diff


$ quilt unapplied


File series fully applied, ends at patch prj.diff


$ quilt top


prj.diff


2.3 修改文件


我们必须将对源代码树所作的任何改动都和一个补丁联系起来。add命令将文件的当前状态与补丁联系起来。add命令的格式为:


quilt add [-P 补丁名] 文件名


如果未指定补丁名,文件就与栈顶补丁联系起来。目前,我们的栈顶补丁是官方补丁。我们不想修改这个补丁,可以用new命令新建一个补丁:


$ quilt new drv_p1.diff


Patch drv_p1.diff is now on top


$ quilt top


drv_p1.diff


$ quilt applied


prj.diff


drv_p1.diff


$ quilt unapplied


File series fully applied, ends at patch drv_p1.diff

然后用add命令向栈顶补丁添加一个准备修改的文件:


$ cd src/drv; quilt add drv2.h


File src/drv/drv2.h added to patch drv_p1.diff

add命令为指定补丁保存了指定文件的当前快照,当我们执行refresh命令时,quilt就会检查文件
的变化,将差异保存到指定补丁中。使用"quilt diff -z [-P 补丁名]
[文件名]"可以查看指定补丁指定文件的当前改动。省略-P参数表示查看当前补丁的改动,省略文件名表示查看所有改动。我们修改drv2.h后,执行
diff命令:


$ quilt diff -z


Index: prj/src/drv/drv2.h


===================================================================


--- prj.orig/src/drv/drv2.h 2008-03-02 13:37:34.000000000 +0800


+++ prj/src/drv/drv2.h 2008-03-02 13:38:53.000000000 +0800


@@ -1,7 +1,7 @@


-#ifndef APP1_H


-#define APP1_H


+#ifndef DRV2_H


+#define DRV2_H


 


-#include "def1.h"+#include "def2.h"

#endif


 


只要文件已经与我们希望保存改动的补丁联系过了,我们就可以多次修改文件。使用"quilt files
[补丁名]"命令可以查看与指定补丁关联的文件。使用"quilt files
-val"可以查看所有补丁联系的所有文件。"-v"参数表示更友好的显示,"-a"参数表示显示所有补丁,"-l"参数显示补丁名。例如:


$ quilt files


src/drv/drv2.h


$ quilt files -val


[prj.diff] src/drv/drv1.h


[prj.diff] src/sys/sys1.c


[prj.diff] src/sys/sys1.h


[prj.diff] src/usr/usr1.c


[prj.diff] src/usr/usr1.h


[drv_p1.diff] src/drv/drv2.h

"quilt refresh [补丁名]"刷新补丁,即将指定补丁的文件变化保存到补丁。省略文件名表示刷新栈顶补丁。我们refresh后,查看补丁文件:


$ quilt refresh


Refreshed patch drv_p1.diff


$ cat ../../patches/drv_p1.diff


Index: prj/src/drv/drv2.h


===================================================================


--- prj.orig/src/drv/drv2.h 2008-03-02 12:42:21.000000000 +0800


+++ prj/src/drv/drv2.h 2008-03-02 12:46:25.000000000 +0800


@@ -1,7 +1,7 @@


-#ifndef APP1_H


-#define APP1_H


+#ifndef DRV2_H


+#define DRV2_H


 


-#include "def1.h"


+#include "def2.h"


 


#endif


 

"quilt diff -z"命令不会显示已经保存的差异。"quilt diff"显示所有的差异,不管是否保存过。


2.4 再做几个补丁


在增加文件前,我们要先将准备增加的文件与补丁联系起来。我们新建一个补丁,然后新增两个文件src/applet/applet1.h和src/applet/applet1.c。


$ cd ..; quilt new more_p1.diff


Patch more_p1.diff is now on top


$ quilt add applet/applet.c


File src/applet/applet.c added to patch more_p1.diff


$ quilt add applet/applet.1


File src/applet/applet.1 added to patch more_p1.diff


 


看看我们增加的文件:


$ quilt files


src/applet/applet.1


src/applet/applet.c


 


哎呀,文件名写错了。我们可以用"remove"命令从补丁中删除关联文件:


$ quilt remove applet/applet.1


rm: remove write-protected regular empty file `.pc/more_p1.diff/src/applet/applet.1'? y


File src/applet/applet.1 removed from patch more_p1.diff


$ quilt remove applet/applet.c


rm: remove write-protected regular empty file `.pc/more_p1.diff/src/applet/applet.c'? y


File src/applet/applet.c removed from patch more_p1.diff


$ quilt files


$ quilt add applet/applet1.h


File src/applet/applet1.h added to patch more_p1.diff


$ quilt add applet/applet1.c


File src/applet/applet1.c added to patch more_p1.diff


$ quilt files


src/applet/applet1.c


src/applet/applet1.h


 


好了,现在可以创建新文件:


$ mkdir applet


$ echo -e "#ifndef APPLET1_H\n#define APPLET1_H\n#include \"def1.h\"\n#endif">applet/applet1.h


$ echo -e "#include \"applet1.h\"">applet/applet1.c


$ quilt refresh more_p1.diff


Refreshed patch more_p1.diff


 


刷新补丁后,我们再修改文件drv2.h。修改前一定要先将文件与准备保存改动的补丁联系起来:


$ quilt add drv/drv2.h


File src/drv/drv2.h added to patch more_p1.diff


$ vi drv/drv2.h


$ quilt diff -z drv/drv2.h


Index: prj/src/drv/drv2.h


===================================================================


--- prj.orig/src/drv/drv2.h 2008-03-02 14:19:35.000000000 +0800


+++ prj/src/drv/drv2.h 2008-03-02 14:31:28.000000000 +0800


@@ -1,7 +1,7 @@


#ifndef DRV2_H


#define DRV2_H


 


-#include "def2.h"


+#include "def1.h"


 


#endif


 

我们再新建一个补丁,然后删除两个文件。删除文件前也要先为文件建立关联:


$ quilt new more_p2.diff


Patch more_p2.diff is now on top


$ quilt add app/*


File src/app/app1.c added to patch more_p2.diff


File src/app/app1.h added to patch more_p2.diff


File src/app/app2.c added to patch more_p2.diff


File src/app/app2.h added to patch more_p2.diff


$ rm -rf app


$ quilt refresh


Refreshed patch more_p2.diff


 


我们再修改applet/applet1.h:


$ quilt edit applet/applet1.h


File src/applet/applet1.h added to patch more_p2.diff


$ quilt refresh


Refreshed patch more_p2.diff


 


"quilt edit"在调用"quilt add"后自动启动编辑器。用refresh命令刷新补丁。


对了,前面为more_p1.diff修改drv2.h后还没有刷新呢。我们查看修改并刷新:



$ quilt diff -z -P more_p1.diff


Index: prj/src/drv/drv2.h


===================================================================


--- prj.orig/src/drv/drv2.h 2008-03-02 14:19:35.000000000 +0800


+++ prj/src/drv/drv2.h 2008-03-02 14:31:28.000000000 +0800


@@ -1,7 +1,7 @@


#ifndef DRV2_H


#define DRV2_H


 


-#include "def2.h"


+#include "def1.h"


 


#endif


 


Warning: more recent patches modify files in patch more_p1.diff


$ quilt refresh more_p1.diff


More recent patches modify files in patch more_p1.diff. Enforce refresh with -f.


$ quilt refresh -f more_p1.diff


Refreshed patch more_p1.diff


quilt会抱怨更新的补丁修改了补丁more_p1.diff的文件。这是在说more_p2.diff修改了applet1.h。我们知道这和我们要刷新的drv2.h没关系,所以可以用-f参数强制刷新。


2.5 管理补丁


series命令可以查看series文件中的补丁:


$ quilt series


prj.diff


drv_p1.diff


more_p1.diff


more_p2.diff

"quilt patches 文件名"显示修改了指定文件的所有补丁,例如:


$ quilt patches drv/drv2.h


drv_p1.diff


more_p1.diff

"quilt annotate 文件名"显示指定文件的修改情况,它会指出哪个补丁修改了哪一行。例如:


$ quilt annotate drv/drv2.h


1 #ifndef DRV2_H


1 #define DRV2_H


 


2 #include "def1.h"





#endif


1 drv_p1.diff


2 more_p1.diff



我们可以使用push和pop命令应用补丁或撤销补丁,例如:


$ quilt pop -a


Removing patch more_p2.diff


Restoring src/app/app1.c


Restoring src/app/app2.c


Restoring src/app/app2.h


Restoring src/app/app1.h


Restoring src/applet/applet1.h


 


Removing patch more_p1.diff


Restoring src/drv/drv2.h


Removing src/applet/applet1.h


Removing src/applet/applet1.c


 


Removing patch drv_p1.diff


Restoring src/drv/drv2.h


 


Removing patch prj.diff


Restoring src/sys/sys1.c


Restoring src/sys/sys1.h


Restoring src/drv/drv1.h


Removing src/usr/usr1.c


Removing src/usr/usr1.h



No patches applied


$ quilt top


No patches applied


$ quilt next


prj.diff


$ quilt previous


No patches applied

"quilt pop -a"撤销所有补丁。top命令显示栈顶命令,即当前应用的最新的补丁。next命令显示下一个可以应用的补丁。previous显示上一条应用过的补丁。"push 补丁A"将从上到下依次应用所有早于补丁A的补丁,最后应用补丁A。例如:


$ quilt push more_p1.diff


Applying patch prj.diff


patching file src/drv/drv1.h


patching file src/sys/sys1.c


patching file src/sys/sys1.h


patching file src/usr/usr1.c


patching file src/usr/usr1.h



Applying patch drv_p1.diff


patching file src/drv/drv2.h



Applying patch more_p1.diff


patching file src/applet/applet1.c


patching file src/applet/applet1.h


patching file src/drv/drv2.h



Now at patch more_p1.diff


$ quilt top


more_p1.diff


$ quilt next


more_p2.diff


$ quilt previous


drv_p1.diff



"quilt push -a"应用所有补丁:


$ quilt push -a


Applying patch more_p2.diff


patching file src/app/app1.c


patching file src/app/app1.h


patching file src/app/app2.c


patching file src/app/app2.h


patching file src/applet/applet1.h



Now at patch more_p2.diff

"quilt graph -all"可以为栈顶补丁的依赖关系生成dot文件。Graphviz的dot可以根据dot文件产生图片,例如:


$ quilt graph --all > ../../more_p2.dot


$ cd ../..; dot -Tpng more_p2.dot -o more_p2.png


2.6 发布补丁


只要将patches目录打包发布就可以了。例如:


$ cd prj; tar cvjf prj-0.1-patches.tar.bz2 patches; mv prj-0.1-patches.tar.bz2 ../..


用户先下载、解压补丁包对应的源代码树:


$ cd ../..; mkdir user; cd user; tar xvjf ../old-prj.tar.bz2; mv old-prj/ prj


然后下载、解压补丁:


$ cd ../..; tar xvjf prj-0.1-patches.tar.bz2; cd user/prj


最后把补丁目录链接到源代码树的patches目录,然后应用所有补丁:


$ ln -sfn ../../patches/ patches


$ quilt push -a


Applying patch prj.diff


patching file src/drv/drv1.h


patching file src/sys/sys1.c


patching file src/sys/sys1.h


patching file src/usr/usr1.c


patching file src/usr/usr1.h

Applying patch drv_p1.diff


patching file src/drv/drv2.h



Applying patch more_p1.diff


patching file src/applet/applet1.c


patching file src/applet/applet1.h


patching file src/drv/drv2.h



Applying patch more_p2.diff


patching file src/app/app1.c


patching file src/app/app1.h


patching file src/app/app2.c


patching file src/app/app2.h


patching file src/applet/applet1.h



Now at patch more_p2.diff

3 结束语


在上面的流程攻略中,我们演示了19个quilt命令:add, annotate, applied,
diff, edit, files, graph, import, new, next, patches, pop, previous,
push, refresh, remove, series, top, unapplied。





2009年6月17日星期三

使用Linux epoll编程



使用Linux epoll编程



在大家苦苦的为在线人数的增长而导致的系统资源吃紧上的问题正在发愁的时候,Linux 2.6内核中提供的System
Epoll为我们提供了一套完美的解决方案。传统的select以及poll的效率会因为在线人数的线形递增而导致呈二次乃至三次方的下降,这些直接导致
了网络服务器可以支持的人数有了个比较明显的限制。

    自从Linux提供了/dev/epoll的设备以及后来2.6内核中对/dev/epoll设备的访问的封装(System
Epoll)之后,这种现象得到了大大的缓解,如果说几个月前,大家还对epoll不熟悉,那么现在来说的话,epoll的应用已经得到了大范围的普及。


    那么究竟如何来使用epoll呢?其实非常简单。


    通过在包含一个头文件#include 以及几个简单的API将可以大大的提高你的网络服务器的支持人数。


    首先通过create_epoll(int
maxfds)来创建一个epoll的句柄,其中maxfds为你epoll所支持的最大句柄数。这个函数会返回一个新的epoll句柄,之后的所有操作
将通过这个句柄来进行操作。在用完之后,记得用close()来关闭这个创建出来的epoll句柄。


    之后在你的网络主循环里面,每一帧的调用epoll_wait(int epfd, epoll_event events, int
max events, int timeout)来查询所有的网络接口,看哪一个可以读,哪一个可以写了。基本的语法为: nfds =
epoll_wait(kdpfd, events, maxevents, -1);


   
其中kdpfd为用epoll_create创建之后的句柄,events是一个epoll_event*的指针,当epoll_wait这个函数操作成
功之后,epoll_events里面将储存所有的读写事件。max_events是当前需要监听的所有socket句柄数。最后一个timeout是
epoll_wait的超时,为0的时候表示马上返回,为-1的时候表示一直等下去,直到有事件范围,为任意正整数的时候表示等这么长的时间,如果一直没
有事件,则范围。一般如果网络主循环是单独的线程的话,可以用-1来等,这样可以保证一些效率,如果是和主逻辑在同一个线程的话,则可以用0来保证主循环
的效率。


    epoll_wait范围之后应该是一个循环,遍利所有的事件:


    for(n = 0; n < nfds; ++n)
      {
        if(events[n].data.fd == listener) { //如果是主socket的事件的话,则表示有新连接进入了,进行新连接的处理。 
       client = accept(listener, (struct sockaddr *) &local, &addrlen);
                   if(client < 0){
                       perror("accept");
                       continue;
                   }
                   setnonblocking(client); // 将新连接置于非阻塞模式
                   ev.events = EPOLLIN | EPOLLET; // 并且将新连接也加入EPOLL的监听队列。


    注意,这里的参数EPOLLIN | EPOLLET并没有设置对写socket的监听,如果有写操作的话,这个时候epoll是不会返回事件的,如果要对写操作也监听的话,应该是EPOLLIN | EPOLLOUT | EPOLLET


    ev.data.fd = client;
    if (epoll_ctl(kdpfd, EPOLL_CTL_ADD, client, &ev) < 0) {
   
//
设置好event之后,将这个新的event通过epoll_ctl加入到epoll的监听队列里面,这里用EPOLL_CTL_ADD来加一个新的
epoll事件,通过EPOLL_CTL_DEL来减少一个epoll事件,通过EPOLL_CTL_MOD来改变一个事件的监听方式。
        fprintf(stderr, "epoll set insertion error: fd=%d0, client);
            return -1;
                   }
               }
      else // 如果不是主socket的事件的话,则代表是一个用户socket的事件,则来处理这个用户socket的事情,比如说read(fd,xxx)之类的,或者一些其他的处理。
          do_use_fd(events[n].data.fd);
}


    对,epoll的操作就这么简单,总共不过4个API:epoll_create, epoll_ctl, epoll_wait和close。 如果您对epoll的效率还不太了解,请参考我之前关于网络游戏的网络编程等相关的文章。


    世界变了,原来担心的问题,现在已经不是问题了。





Memcached深度分析



Memcached深度分析



Memcached是danga.com(运营LiveJournal的技术团队)开发的一套分布式内存对象缓存系统,
用于在动态系统中减少数据库负载,提升性能。关于这个东西,相信很多人都用过,本文意在通过对memcached的实现及代码分析,获得对这个出色的开源
软件更深入的了解,并可以根据我们的需要对其进行更进一步的优化。末了将通过对BSM_Memcache扩展的分析,加深对memcached的使用方式
理解。


本文的部分内容可能需要比较好的数学基础作为辅助。


◎Memcached是什么


在阐述这个问题之前,我们首先要清楚它“不是什么”。很多人把它当作和SharedMemory那种形式的存储载体来使
用,虽然memcached使用了同样的“Key=>Value”方式组织数据,但是它和共享内存、APC等本地缓存有非常大的区别。
Memcached是分布式的,也就是说它不是本地的。它基于网络连接(当然它也可以使用localhost)方式完成服务,本身它是一个独立于应用的程
序或守护进程(Daemon方式)。


Memcached使用libevent库实现网络连接服务,理论上可以处理无限多的连接,但是它和Apache不同,
它更多的时候是面向稳定的持续连接的,所以它实际的并发能力是有限制的。在保守情况下memcached的最大同时连接数为200,这和Linux线程能
力有关系,这个数值是可以调整的。关于libevent可以参考相关文档。
Memcached内存使用方式也和APC不同。APC是基于共享内存和MMAP的,memcachd有自己的内存分配算法和管理方式,它和共享内存没有
关系,也没有共享内存的限制,通常情况下,每个memcached进程可以管理2GB的内存空间,如果需要更多的空间,可以增加进程数。


◎Memcached适合什么场合


在很多时候,memcached都被滥用了,这当然少不了对它的抱怨。我经常在论坛上看见有人发贴,类似于“如何提高效率”,回复是“用memcached”,至于怎么用,用在哪里,用来干什么一句没有。memcached不是万能的,它也不是适用在所有场合。


Memcached是“分布式”的内存对象缓存系统,那么就是说,那些不需要“分布”的,不需要共享的,或者干脆规模小
到只有一台服务器的应用,memcached不会带来任何好处,相反还会拖慢系统效率,因为网络连接同样需要资源,即使是UNIX本地连接也一样。
在我之前的测试数据中显示,memcached本地读写速度要比直接PHP内存数组慢几十倍,而APC、共享内存方式都和直接数组差不多。可见,如果只是
本地级缓存,使用memcached是非常不划算的。


Memcached在很多时候都是作为数据库前端cache使用的。因为它比数据库少了很多SQL解析、磁盘操作等开
销,而且它是使用内存来管理数据的,所以它可以提供比直接读取数据库更好的性能,在大型系统中,访问同样的数据是很频繁的,memcached可以大大降
低数据库压力,使系统执行效率提升。另外,memcached也经常作为服务器之间数据共享的存储媒介,例如在SSO系统中保存系统单点登陆状态的数据就
可以保存在memcached中,被多个应用共享。


需要注意的是,memcached使用内存管理数据,所以它是易失的,当服务器重启,或者memcached进程中止,
数据便会丢失,所以memcached不能用来持久保存数据。很多人的错误理解,memcached的性能非常好,好到了内存和硬盘的对比程度,其实
memcached使用内存并不会得到成百上千的读写速度提高,它的实际瓶颈在于网络连接,它和使用磁盘的数据库系统相比,好处在于它本身非常“轻”,因
为没有过多的开销和直接的读写方式,它可以轻松应付非常大的数据交换量,所以经常会出现两条千兆网络带宽都满负荷了,memcached进程本身并不占用
多少CPU资源的情况。


◎Memcached的工作方式


以下的部分中,读者最好能准备一份memcached的源代码。


Memcached是传统的网络服务程序,如果启动的时候使用了-d参数,它会以守护进程的方式执行。创建守护进程由daemon.c完成,这个程序只有一个daemon函数,这个函数很简单(如无特殊说明,代码以1.2.1为准):









#include <fcntl.h>
#include <stdlib.h>
#include <unistd.h>

int
daemon(nochdir, noclose)
int nochdir, noclose;
{
int fd;

switch (fork()) {
case -1:
return (-1);
case 0:
break;
default:
_exit(0);
}

if (setsid() == -1)
return (-1);

if (!nochdir)
(void)chdir("/");

if (!noclose && (fd = open("/dev/null", O_RDWR, 0)) != -1) {
(void)dup2(fd, STDIN_FILENO);
(void)dup2(fd, STDOUT_FILENO);
(void)dup2(fd, STDERR_FILENO);
if (fd > STDERR_FILENO)
(void)close(fd);
}
return (0);
}


这个函数 fork 了整个进程之后,父进程就退出,接着重新定位 STDIN 、 STDOUT 、 STDERR 到空设备, daemon 就建立成功了。


Memcached 本身的启动过程,在 memcached.c 的 main 函数中顺序如下:


1 、调用 settings_init() 设定初始化参数
2 、从启动命令中读取参数来设置 setting 值
3 、设定 LIMIT 参数
4 、开始网络 socket 监听(如果非 socketpath 存在)( 1.2 之后支持 UDP 方式)
5 、检查用户身份( Memcached 不允许 root 身份启动)
6 、如果有 socketpath 存在,开启 UNIX 本地连接(Sock 管道)
7 、如果以 -d 方式启动,创建守护进程(如上调用 daemon 函数)
8 、初始化 item 、 event 、状态信息、 hash 、连接、 slab
9 、如设置中 managed 生效,创建 bucket 数组
10 、检查是否需要锁定内存页
11 、初始化信号、连接、删除队列
12 、如果 daemon 方式,处理进程 ID
13 、event 开始,启动过程结束, main 函数进入循环。


在 daemon 方式中,因为 stderr 已经被定向到黑洞,所以不会反馈执行中的可见错误信息。


memcached.c 的主循环函数是 drive_machine ,传入参数是指向当前的连接的结构指针,根据 state 成员的状态来决定动作。


Memcached 使用一套自定义的协议完成数据交换,它的 protocol 文档可以参考: http://code.sixapart.com/svn/memcached/trunk/server/doc/protocol.txt


在API中,换行符号统一为\r\n


◎Memcached的内存管理方式


Memcached有一个很有特色的内存管理方式,为了提高效率,它使用预申请和分组的方式管理内存空间,而并不是每次需要写入数据的时候去malloc,删除数据的时候free一个指针。Memcached使用slab->chunk的组织方式管理内存。


1.1和1.2的slabs.c中的slab空间划分算法有一些不同,后面会分别介绍。


Slab可以理解为一个内存块,一个slab是memcached一次申请内存的最小单位,在memcached中,一
个slab的大小默认为1048576字节(1MB),所以memcached都是整MB的使用内存。每一个slab被划分为若干个chunk,每个
chunk里保存一个item,每个item同时包含了item结构体、key和value(注意在memcached中的value是只有字符串的)。
slab按照自己的id分别组成链表,这些链表又按id挂在一个slabclass数组上,整个结构看起来有点像二维数组。slabclass的长度在
1.1中是21,在1.2中是200。


slab有一个初始chunk大小,1.1中是1字节,1.2中是80字节,1.2中有一个factor值,默认为1.25


在1.1中,chunk大小表示为初始大小*2^n,n为classid,即:id为0的slab,每chunk大小1
字节,id为1的slab,每chunk大小2字节,id为2的slab,每chunk大小4字节……id为20的slab,每chunk大小为1MB,
就是说id为20的slab里只有一个chunk:









void slabs_init(size_t limit) {
int i;
int size=1;

mem_limit = limit;
for(i=0; i<=POWER_LARGEST; i++, size*=2) {
slabclass[i].size = size;
slabclass[i].perslab = POWER_BLOCK / size;
slabclass[i].slots = 0;
slabclass[i].sl_curr = slabclass[i].sl_total = slabclass[i].slabs = 0;
slabclass[i].end_page_ptr = 0;
slabclass[i].end_page_free = 0;
slabclass[i].slab_list = 0;
slabclass[i].list_size = 0;
slabclass[i].killing = 0;
}

/* for the test suite: faking of how much we've already malloc'd */
{
char *t_initial_malloc = getenv("T_MEMD_INITIAL_MALLOC");
if (t_initial_malloc) {
mem_malloced = atol(getenv("T_MEMD_INITIAL_MALLOC"));
}
}

/* pre-allocate slabs by default, unless the environment variable
for testing is set to something non-zero */
{
char *pre_alloc = getenv("T_MEMD_SLABS_ALLOC");
if (!pre_alloc || atoi(pre_alloc)) {
slabs_preallocate(limit / POWER_BLOCK);
}
}
}


在1.2中,chunk大小表示为初始大小*f^n,f为factor,在memcached.c中定义,n为
classid,同时,201个头不是全部都要初始化的,因为factor可变,初始化只循环到计算出的大小达到slab大小的一半为止,而且它是从
id1开始的,即:id为1的slab,每chunk大小80字节,id为2的slab,每chunk大小80*f,id为3的slab,每chunk大
小80*f^2,初始化大小有一个修正值CHUNK_ALIGN_BYTES,用来保证n-byte排列
(保证结果是CHUNK_ALIGN_BYTES的整倍数)。这样,在标准情况下,memcached1.2会初始化到id40,这个slab中每个
chunk大小为504692,每个slab中有两个chunk。最后,slab_init函数会在最后补足一个id41,它是整块的,也就是这个
slab中只有一个1MB大的chunk:









void slabs_init(size_t limit, double factor) {
int i = POWER_SMALLEST - 1;
unsigned int size = sizeof(item) + settings.chunk_size;

/* Factor of 2.0 means use the default memcached behavior */
if (factor == 2.0 && size < 128)
size = 128;

mem_limit = limit;
memset(slabclass, 0, sizeof(slabclass));

while (++i < POWER_LARGEST && size <= POWER_BLOCK / 2) {
/* Make sure items are always n-byte aligned */
if (size % CHUNK_ALIGN_BYTES)
size += CHUNK_ALIGN_BYTES - (size % CHUNK_ALIGN_BYTES);

slabclass[i].size = size;
slabclass[i].perslab = POWER_BLOCK / slabclass[i].size;
size *= factor;
if (settings.verbose > 1) {
fprintf(stderr, "slab class %3d: chunk size %6d perslab %5d\n",
i, slabclass[i].size, slabclass[i].perslab);
}
}

power_largest = i;
slabclass[power_largest].size = POWER_BLOCK;
slabclass[power_largest].perslab = 1;

/* for the test suite: faking of how much we've already malloc'd */
{
char *t_initial_malloc = getenv("T_MEMD_INITIAL_MALLOC");
if (t_initial_malloc) {
mem_malloced = atol(getenv("T_MEMD_INITIAL_MALLOC"));
}

}

#ifndef DONT_PREALLOC_SLABS
{
char *pre_alloc = getenv("T_MEMD_SLABS_ALLOC");
if (!pre_alloc || atoi(pre_alloc)) {
slabs_preallocate(limit / POWER_BLOCK);
}
}
#endif
}


由上可以看出,memcached的内存分配是有冗余的,当一个slab不能被它所拥有的chunk大小整除时,slab尾部剩余的空间就被丢弃了,如id40中,两个chunk占用了1009384字节,这个slab一共有1MB,那么就有39192字节被浪费了。


Memcached使用这种方式来分配内存,是为了可以快速的通过item长度定位出slab的classid,有一点
类似hash,因为item的长度是可以计算的,比如一个item的长度是300字节,在1.2中就可以得到它应该保存在id7的slab中,因为按照上
面的计算方法,id6的chunk大小是252字节,id7的chunk大小是316字节,id8的chunk大小是396字节,表示所有252到316
字节的item都应该保存在id7中。同理,在1.1中,也可以计算得到它出于256和512之间,应该放在chunk_size为512的id9中
(32位系统)。


Memcached初始化的时候,会初始化slab(前面可以看到,在main函数中调用了
slabs_init())。它会在slabs_init()中检查一个常量DONT_PREALLOC_SLABS,如果这个没有被定义,说明使用预分
配内存方式初始化slab,这样在所有已经定义过的slabclass中,每一个id创建一个slab。这样就表示,1.2在默认的环境中启动进程后要分
配41MB的slab空间,在这个过程里,memcached的第二个内存冗余发生了,因为有可能一个id根本没有被使用过,但是它也默认申请了一个
slab,每个slab会用掉1MB内存


当一个slab用光后,又有新的item要插入这个id,那么它就会重新申请新的slab,申请新的slab时,对应id的slab链表就要增长,这个链表是成倍增长的,在函数grow_slab_list函数中,这个链的长度从1变成2,从2变成4,从4变成8……:









static int grow_slab_list (unsigned int id) {
slabclass_t *p = &slabclass[id];
if (p->slabs == p->list_size) {
size_t new_size = p->list_size ? p->list_size * 2 : 16;
void *new_list = realloc(p->slab_list, new_size*sizeof(void*));
if (new_list == 0) return 0;
p->list_size = new_size;
p->slab_list = new_list;
}
return 1;
}


在定位item时,都是使用slabs_clsid函数,传入参数为item大小,返回值为classid,由这个过程
可以看出,memcached的第三个内存冗余发生在保存item的过程中,item总是小于或等于chunk大小的,当item小于chunk大小时,
就又发生了空间浪费。


◎Memcached的NewHash算法


Memcached的item保存基于一个大的hash表,它的实际地址就是slab中的chunk偏移,但是它的定位
是依靠对key做hash的结果,在primary_hashtable中找到的。在assoc.c和items.c中定义了所有的hash和item操
作。


Memcached使用了一个叫做NewHash的算法,它的效果很好,效率也很高。1.1和1.2的NewHash有一些不同,主要的实现方式还是一样的,1.2的hash函数是经过整理优化的,适应性更好一些。


NewHash的原型参考:http://burtleburtle.net/bob/hash/evahash.html。数学家总是有点奇怪,呵呵~


为了变换方便,定义了u4和u1两种数据类型,u4就是无符号的长整形,u1就是无符号char(0-255)。


具体代码可以参考1.1和1.2源码包。


注意这里的hashtable长度,1.1和1.2也是有区别的,1.1中定义了HASHPOWER常量为
20,hashtable表长为hashsize(HASHPOWER),就是4MB(hashsize是一个宏,表示1右移n位),1.2中是变量
16,即hashtable表长65536:









typedef  unsigned long  int  ub4;   /* unsigned 4-byte quantities */
typedef unsigned char ub1; /* unsigned 1-byte quantities */

#define hashsize(n) ((ub4)1<<(n))
#define hashmask(n) (hashsize(n)-1)


在assoc_init()中,会对primary_hashtable做初始化,对应的hash操作包
括:assoc_find()、assoc_expand()、assoc_move_next_bucket()、assoc_insert()、
assoc_delete(),对应于item的读写操作。其中assoc_find()是根据key和key长寻找对应的item地址的函数(注意在C
中,很多时候都是同时直接传入字符串和字符串长度,而不是在函数内部做strlen),返回的是item结构指针,它的数据地址在slab中的某个
chunk上。


items.c是数据项的操作程序,每一个完整的item包括几个部分,在item_make_header()中定义为:


key:键
nkey:键长
flags:用户定义的flag(其实这个flag在memcached中没有启用)
nbytes:值长(包括换行符号\r\n)
suffix:后缀Buffer
nsuffix:后缀长


一个完整的item长度是键长+值长+后缀长+item结构大小(32字节),item操作就是根据这个长度来计算slab的classid的。


hashtable中的每一个桶上挂着一个双链表,item_init()的时候已经初始化了heads、tails、
sizes三个数组为0,这三个数组的大小都为常量LARGEST_ID(默认为255,这个值需要配合factor来修改),在每次
item_assoc()的时候,它会首先尝试从slab中获取一块空闲的chunk,如果没有可用的chunk,会在链表中扫描50次,以得到一个被
LRU踢掉的item,将它unlink,然后将需要插入的item插入链表中。


注意item的refcount成员。item被unlink之后只是从链表上摘掉,不是立刻就被free的,只是将它放到删除队列中(item_unlink_q()函数)。


item对应一些读写操作,包括remove、update、replace,当然最重要的就是alloc操作。


item还有一个特性就是它有过期时间,这是memcached的一个很有用的特性,很多应用都是依赖于
memcached的item过期,比如session存储、操作锁等。item_flush_expired()函数就是扫描表中的item,对过期的
item执行unlink操作,当然这只是一个回收动作,实际上在get的时候还要进行时间判断:









/* expires items that are more recent than the oldest_live setting. */
void item_flush_expired() {
int i;
item *iter, *next;
if (! settings.oldest_live)
return;
for (i = 0; i < LARGEST_ID; i++) {
/* The LRU is sorted in decreasing time order, and an item's timestamp
* is never newer than its last access time, so we only need to walk
* back until we hit an item older than the oldest_live time.
* The oldest_live checking will auto-expire the remaining items.
*/
for (iter = heads[i]; iter != NULL; iter = next) {
if (iter->time >= settings.oldest_live) {
next = iter->next;
if ((iter->it_flags & ITEM_SLABBED) == 0) {
item_unlink(iter);
}
} else {
/* We've hit the first old item. Continue to the next queue. */
break;
}
}
}
}









/* wrapper around assoc_find which does the lazy expiration/deletion logic */
item *get_item_notedeleted(char *key, size_t nkey, int *delete_locked) {
item *it = assoc_find(key, nkey);
if (delete_locked) *delete_locked = 0;
if (it && (it->it_flags & ITEM_DELETED)) {
/* it's flagged as delete-locked. let's see if that condition
is past due, and the 5-second delete_timer just hasn't
gotten to it yet... */
if (! item_delete_lock_over(it)) {
if (delete_locked) *delete_locked = 1;
it = 0;
}
}
if (it && settings.oldest_live && settings.oldest_live <= current_time &&
it->time <= settings.oldest_live) {
item_unlink(it);
it = 0;
}
if (it && it->exptime && it->exptime <= current_time) {
item_unlink(it);
it = 0;
}
return it;
}


Memcached的内存管理方式是非常精巧和高效的,它很大程度上减少了直接alloc系统内存的次数,降低函数开销和内存碎片产生几率,虽然这种方式会造成一些冗余浪费,但是这种浪费在大型系统应用中是微不足道的。










结构看起来是这个样子的

◎Memcached的理论参数计算方式


影响 memcached 工作的几个参数有:


常量REALTIME_MAXDELTA 60*60*24*30
最大30天的过期时间


conn_init()中的freetotal(=200)
最大同时连接数


常量KEY_MAX_LENGTH 250
最大键长


settings.factor(=1.25)
factor将影响chunk的步进大小


settings.maxconns(=1024)
最大软连接


settings.chunk_size(=48)
一个保守估计的key+value长度,用来生成id1中的chunk长度(1.2)。id1的chunk长度等于这个数值加上item结构体的长度(32),即默认的80字节。


常量POWER_SMALLEST 1
最小classid(1.2)


常量POWER_LARGEST 200
最大classid(1.2)


常量POWER_BLOCK 1048576
默认slab大小


常量CHUNK_ALIGN_BYTES (sizeof(void *))
保证chunk大小是这个数值的整数倍,防止越界(void *的长度在不同系统上不一样,在标准32位系统上是4)


常量ITEM_UPDATE_INTERVAL 60
队列刷新间隔


常量LARGEST_ID 255
最大item链表数(这个值不能比最大的classid小)


变量hashpower(在1.1中是常量HASHPOWER)
决定hashtable的大小


根据上面介绍的内容及参数设定,可以计算出的一些结果:


1、在memcached中可以保存的item个数是没有软件上限的,之前我的100万的说法是错误的。
2、假设NewHash算法碰撞均匀,查找item的循环次数是item总数除以hashtable大小(由hashpower决定),是线性的。
3、Memcached限制了可以接受的最大item是1MB,大于1MB的数据不予理会。
4、Memcached的空间利用率和数据特性有很大的关系,又与DONT_PREALLOC_SLABS常量有关。 在最差情况下,有198个slab会被浪费(所有item都集中在一个slab中,199个id全部分配满)。


◎Memcached的定长优化


根据上面几节的描述,多少对memcached有了一个比较深入的认识。在深入认识的基础上才好对它进行优化。


Memcached本身是为变长数据设计的,根据数据特性,可以说它是“面向大众”的设计,但是很多时候,我们的数据并
不是这样的“普遍”,典型的情况中,一种是非均匀分布,即数据长度集中在几个区域内(如保存用户
Session);另一种更极端的状态是等长数据(如定长键值,定长数据,多见于访问、在线统计或执行锁)。


这里主要研究一下定长数据的优化方案(1.2),集中分布的变长数据仅供参考,实现起来也很容易。


解决定长数据,首先需要解决的是slab的分配问题,第一个需要确认的是我们不需要那么多不同chunk长度的slab,为了最大限度地利用资源,最好chunk和item等长,所以首先要计算item长度。


在之前已经有了计算item长度的算法,需要注意的是,除了字符串长度外,还要加上item结构的长度32字节。


假设我们已经计算出需要保存200字节的等长数据。


接下来是要修改slab的classid和chunk长度的关系。在原始版本中,chunk长度和classid是有对
应关系的,现在如果把所有的chunk都定为200个字节,那么这个关系就不存在了,我们需要重新确定这二者的关系。一种方法是,整个存储结构只使用一个
固定的id,即只使用199个槽中的1个,在这种条件下,就一定要定义DONT_PREALLOC_SLABS来避免另外的预分配浪费。另一种方法是建立
一个hash关系,来从item确定classid,不能使用长度来做键,可以使用key的NewHash结果等不定数据,或者直接根据key来做
hash(定长数据的key也一定等长)。这里简单起见,选择第一种方法,这种方法的不足之处在于只使用一个id,在数据量非常大的情况下,slab链会
很长(因为所有数据都挤在一条链上了),遍历起来的代价比较高。


前面介绍了三种空间冗余,设置chunk长度等于item长度,解决了第一种空间浪费问题,不预申请空间解决了第二种空
间浪费问题,那么对于第一种问题(slab内剩余)如何解决呢,这就需要修改POWER_BLOCK常量,使得每一个slab大小正好等于chunk长度
的整数倍,这样一个slab就可以正好划分成n个chunk。这个数值应该比较接近1MB,过大的话同样会造成冗余,过小的话会造成次数过多的
alloc,根据chunk长度为200,选择1000000作为POWER_BLOCK的值,这样一个slab就是100万字节,不是1048576。
三个冗余问题都解决了,空间利用率会大大提升。


修改 slabs_clsid 函数,让它直接返回一个定值(比如 1 ):









unsigned int slabs_clsid(size_t size) {
return 1;
}


修改slabs_init函数,去掉循环创建所有classid属性的部分,直接添加slabclass[1]:









slabclass[1].size = 200;		//每chunk200字节
slabclass[1].perslab = 5000; //1000000/200


◎Memcached客户端


Memcached是一个服务程序,使用的时候可以根据它的协议,连接到memcached服务器上,发送命令给服务进
程,就可以操作上面的数据。为了方便使用,memcached有很多个客户端程序可以使用,对应于各种语言,有各种语言的客户端。基于C语言的有
libmemcache、APR_Memcache;基于Perl的有Cache::Memcached;另外还有Python、Ruby、Java、
C#等语言的支持。PHP的客户端是最多的,不光有mcache和PECL
memcache两个扩展,还有大把的由PHP编写的封装类,下面介绍一下在PHP中使用memcached的方法:


mcache扩展是基于libmemcache再封装的。libmemcache一直没有发布stable版本,目前版本是1.4.0-rc2,可以在这里找到。libmemcache有一个很不好的特性,就是会向stderr写很多错误信息,一般的,作为lib使用的时候,stderr一般都会被定向到其它地方,比如Apache的错误日志,而且libmemcache会自杀,可能会导致异常,不过它的性能还是很好的。


mcache扩展最后更新到1.2.0-beta10,作者大概是离职了,不光停止更新,连网站也打不开了(~_~),
只能到其它地方去获取这个不负责的扩展了。解压后安装方法如常:phpize & configure & make &
make install,一定要先安装libmemcache。使用这个扩展很简单:









<?php
$mc = memcache(); // 创建一个memcache连接对象,注意这里不是用new!
$mc->add_server('localhost', 11211); // 添加一个服务进程
$mc->add_server('localhost', 11212); // 添加第二个服务进程
$mc->set('key1', 'Hello'); // 写入key1 => Hello
$mc->set('key2', 'World', 10); // 写入key2 => World,10秒过期
$mc->set('arr1', array('Hello', 'World')); // 写入一个数组
$key1 = $mc->get('key1'); // 获取'key1'的值,赋给$key1
$key2 = $mc->get('key2'); // 获取'key2'的值,赋给$key2,如果超过10秒,就取不到了
$arr1 = $mc->get('arr1'); // 获取'arr1'数组
$mc->delete('arr1'); // 删除'arr1'
$mc->flush_all(); // 删掉所有数据
$stats = $mc->stats(); // 获取服务器信息
var_dump($stats); // 服务器信息是一个数组
?>


这个扩展的好处是可以很方便地实现分布式存储和负载均衡,因为它可以添加多个服务地址,数据在保存的时候是会根据
hash结果定位到某台服务器上的,这也是libmemcache的特性。libmemcache支持集中hash方式,包括CRC32、ELF和
Perl hash。


PECL memcache是PECL发布的扩展,目前最新版本是2.1.0,可以在pecl网站得到。memcache扩展的使用方法可以在新一些的PHP手册中找到,它和mcache很像,真的很像:









<?php

$memcache = new Memcache;
$memcache->connect('localhost', 11211) or die ("Could not connect");

$version = $memcache->getVersion();
echo "Server's version: ".$version."\n";

$tmp_object = new stdClass;
$tmp_object->str_attr = 'test';
$tmp_object->int_attr = 123;

$memcache->set('key', $tmp_object, false, 10) or die ("Failed to save data at the server");
echo "Store data in the cache (data will expire in 10 seconds)\n";

$get_result = $memcache->get('key');
echo "Data from the cache:\n";

var_dump($get_result);

?>


这个扩展是使用php的stream直接连接memcached服务器并通过socket发送命令的。它不像
libmemcache那样完善,也不支持add_server这种分布操作,但是因为它不依赖其它的外界程序,兼容性要好一些,也比较稳定。至于效率,
差别不是很大。


另外,有很多的PHP class可以使用,比如MemcacheClient.inc.php,phpclasses.org上可以找到很多,一般都是对perl client API的再封装,使用方式很像。


◎BSM_Memcache


从C
client来说,APR_Memcache是一个很成熟很稳定的client程序,支持线程锁和原子级操作,保证运行的稳定性。不过它是基于APR的
(APR将在最后一节介绍),没有libmemcache的应用范围广,目前也没有很多基于它开发的程序,现有的多是一些Apache
Module,因为它不能脱离APR环境运行。但是APR倒是可以脱离Apache单独安装的,在APR网站上可以下载APR和APR-util,不需要有Apache,可以直接安装,而且它是跨平台的。


BSM_Memcache是我在BS.Magic项目中开发的一个基于APR_Memcache的PHP扩展,说起来有点拗口,至少它把APR扯进了PHP扩展中。这个程序很简单,也没做太多的功能,只是一种形式的尝试,它支持服务器分组。


和mcache扩展支持多服务器分布存储不同,BSM_Memcache支持多组服务器,每一组内的服务器还是按照
hash方式来分布保存数据,但是两个组中保存的数据是一样的,也就是实现了热备,它不会因为一台服务器发生单点故障导致数据无法获取,除非所有的服务器
组都损坏(例如机房停电)。当然实现这个功能的代价就是性能上的牺牲,在每次添加删除数据的时候都要扫描所有的组,在get数据的时候会随机选择一组服务
器开始轮询,一直到找到数据为止,正常情况下一次就可以获取得到。


BSM_Memcache只支持这几个函数:









zend_function_entry bsm_memcache_functions[] =
{
PHP_FE(mc_get, NULL)
PHP_FE(mc_set, NULL)
PHP_FE(mc_del, NULL)
PHP_FE(mc_add_group, NULL)
PHP_FE(mc_add_server, NULL)
PHP_FE(mc_shutdown, NULL)
{NULL, NULL, NULL}
};


mc_add_group函数返回一个整形(其实应该是一个object,我偷懒了~_~)作为组ID,mc_add_server的时候要提供两个参数,一个是组ID,一个是服务器地址(ADDR:PORT)。









/**
* Add a server group
*/
PHP_FUNCTION(mc_add_group)
{
apr_int32_t group_id;
apr_status_t rv;

if (0 != ZEND_NUM_ARGS())
{
WRONG_PARAM_COUNT;
RETURN_NULL();
}

group_id = free_group_id();
if (-1 == group_id)
{
RETURN_FALSE;
}

apr_memcache_t *mc;
rv = apr_memcache_create(p, MAX_G_SERVER, 0, &mc);

add_group(group_id, mc);

RETURN_DOUBLE(group_id);
}









/**
* Add a server into group
*/
PHP_FUNCTION(mc_add_server)
{
apr_status_t rv;
apr_int32_t group_id;
double g;
char *srv_str;
int srv_str_l;

if (2 != ZEND_NUM_ARGS())
{
WRONG_PARAM_COUNT;
}

if (zend_parse_parameters(ZEND_NUM_ARGS() TSRMLS_CC, "ds", &g, &srv_str, &srv_str_l) == FAILURE)
{
RETURN_FALSE;
}

group_id = (apr_int32_t) g;

if (-1 == is_validate_group(group_id))
{
RETURN_FALSE;
}

char *host, *scope;
apr_port_t port;

rv = apr_parse_addr_port(&host, &scope, &port, srv_str, p);
if (APR_SUCCESS == rv)
{
// Create this server object
apr_memcache_server_t *st;
rv = apr_memcache_server_create(p, host, port, 0, 64, 1024, 600, &st);
if (APR_SUCCESS == rv)
{
if (NULL == mc_groups[group_id])
{
RETURN_FALSE;
}

// Add server
rv = apr_memcache_add_server(mc_groups[group_id], st);

if (APR_SUCCESS == rv)
{
RETURN_TRUE;
}
}
}

RETURN_FALSE;
}


在set和del数据的时候,要循环所有的组:









/**
* Store item into all groups
*/
PHP_FUNCTION(mc_set)
{
char *key, *value;
int key_l, value_l;
double ttl = 0;
double set_ct = 0;

if (2 != ZEND_NUM_ARGS())
{
WRONG_PARAM_COUNT;
}

if (zend_parse_parameters(ZEND_NUM_ARGS() TSRMLS_CC, "ss|d", &key, &key_l, &value, &value_l, ttl) == FAILURE)
{
RETURN_FALSE;
}

// Write data into every object
apr_int32_t i = 0;
if (ttl < 0)
{
ttl = 0;
}

apr_status_t rv;

for (i = 0; i < MAX_GROUP; i++)
{
if (0 == is_validate_group(i))
{
// Write it!
rv = apr_memcache_add(mc_groups[i], key, value, value_l, (apr_uint32_t) ttl, 0);
if (APR_SUCCESS == rv)
{
set_ct++;
}
}
}

RETURN_DOUBLE(set_ct);
}


在mc_get中,首先要随机选择一个组,然后从这个组开始轮询:









/**
* Fetch a item from a random group
*/
PHP_FUNCTION(mc_get)
{
char *key, *value = NULL;
int key_l;
apr_size_t value_l;

if (1 != ZEND_NUM_ARGS())
{
WRONG_PARAM_COUNT;
}

if (zend_parse_parameters(ZEND_NUM_ARGS() TSRMLS_CC, "s", &key, &key_l) == FAILURE)
{
RETURN_MULL();
}

// I will try ...
// Random read
apr_int32_t curr_group_id = random_group();
apr_int32_t i = 0;
apr_int32_t try = 0;
apr_uint32_t flag;
apr_memcache_t *oper;
apr_status_t rv;

for (i = 0; i < MAX_GROUP; i++)
{
try = i + curr_group_id;
try = try % MAX_GROUP;
if (0 == is_validate_group(try))
{
// Get a value
oper = mc_groups[try];
rv = apr_memcache_getp(mc_groups[try], p, (const char *) key, &value, &value_l, 0);
if (APR_SUCCESS == rv)
{
RETURN_STRING(value, 1);
}
}
}

RETURN_FALSE;
}









/**
* Random group id
* For mc_get()
*/
apr_int32_t random_group()
{
struct timeval tv;
struct timezone tz;
int usec;

gettimeofday(&tv, &tz);

usec = tv.tv_usec;

int curr = usec % count_group();

return (apr_int32_t) curr;
}


BSM_Memcache的使用方式和其它的client类似:









<?php
$g1 = mc_add_group(); // 添加第一个组
$g2 = mc_add_group(); // 添加第二个组
mc_add_server($g1, 'localhost:11211'); // 在第一个组中添加第一台服务器
mc_add_server($g1, 'localhost:11212'); // 在第一个组中添加第二台服务器
mc_add_server($g2, '10.0.0.16:11211'); // 在第二个组中添加第一台服务器
mc_add_server($g2, '10.0.0.17:11211'); // 在第二个组中添加第二台服务器

mc_set('key', 'Hello'); // 写入数据
$key = mc_get('key'); // 读出数据
mc_del('key'); // 删除数据
mc_shutdown(); // 关闭所有组
?>


APR_Memcache的相关资料可以在这里找到,BSM_Memcache可以在本站下载


◎APR环境介绍


APR的全称:Apache Portable Runtime。它是Apache软件基金会
建并维持的一套跨平台的C语言库。它从Apache httpd1.x中抽取出来并独立于httpd之外,Apache
httpd2.x就是建立在APR上。APR提供了很多方便的API接口可供使用,包括如内存池、字符串操作、网络、数组、hash表等实用的功能。开发
Apache2 Module要接触很多APR函数,当然APR可以独立安装独立使用,可以用来写自己的应用程序,不一定是Apache
httpd的相关开发。




2009年5月19日星期二

全面剖析XMLHttpRequest对象

全面剖析XMLHttpRequest对象


  XMLHttpRequest对象是当今所有AJAX和Web 2.0应用程序的技术基础。尽管软件经销商和开源社团现在都在提供各种AJAX框架以进一步简化XMLHttpRequest对象的使用;但是,我们仍然很有必要理解这个对象的详细工作机制。

  一、 引言

 
 异步JavaScript与XML(AJAX)是一个专用术语,用于实现在客户端脚本与服务器之间的数据交互过程。这一技术的优点在于,它向开发者提供
了一种从Web服务器检索数据而不必把用户当前正在观察的页面回馈给服务器。与现代浏览器的通过存取浏览器DOM结构的编程代码(JavaScript)
动态地改变被显示内容的支持相配合,AJAX让开发者在浏览器端更新被显示的HTML内容而不必刷新页面。换句话说,AJAX可以使基于浏览器的应用程序
更具交互性而且更类似传统型桌面应用程序。

  Google的Gmail和Outlook Express就是两个使用AJAX技术的我们所熟悉的例子。而且,AJAX可以用于任何客户端脚本语言中,这包括JavaScript,Jscript和VBScript。

 
 AJAX利用一个构建到所有现代浏览器内部的对象-XMLHttpRequest-来实现发送和接收HTTP请求与响应信息。一个经由
XMLHttpRequest对象发送的HTTP请求并不要求页面中拥有或回寄一个<form>元素。AJAX中的"A"代表了"异步",这意味着
XMLHttpRequest对象的send()方法可以立即返回,从而让Web页面上的其它HTML/JavaScript继续其浏览器端处理而由服务
器处理HTTP请求并发送响应。尽管缺省情况下请求是异步进行的,但是,你可以选择发送同步请求,这将会暂停其它Web页面的处理,直到该页面接收到服务
器的响应为止。

  微软在其Internet Explorer(IE)
5中作为一个ActiveX对象形式引入了XMLHttpRequest对象。其他的认识到这一对象重要性的浏览器制造商也都纷纷在他们的浏览器内实现了
XMLHttpRequest对象,但是作为一个本地JavaScript对象而不是作为一个ActiveX对象实现。而如今,在认识到实现这一类型的价
值及安全性特征之后,微软已经在其IE
7中把XMLHttpRequest实现为一个窗口对象属性。幸运的是,尽管其实现(因而也影响到调用方式)细节不同,但是,所有的浏览器实现都具有类似
的功能,并且实质上是相同方法。目前,W3C组织正在努力进行XMLHttpRequest对象的标准化,并且已经发行了有关该W3C规范的一个草案。

  本文将对XMLHttpRequest对象API进行详细讨论,并将解释其所有的属性和方法。

  二、 XMLHttpRequest对象的属性和事件

  XMLHttpRequest对象暴露各种属性、方法和事件以便于脚本处理和控制HTTP请求与响应。下面,我们将对此展开详细的讨论。
readyState属性

 
 当XMLHttpRequest对象把一个HTTP请求发送到服务器时将经历若干种状态:一直等待直到请求被处理;然后,它才接收一个响应。这样以来,
脚本才正确响应各种状态-XMLHttpRequest对象暴露一个描述对象的当前状态的readyState属性,如表格1所示。

  表格1.XMLHttpRequest对象的ReadyState属性值列表。





















ReadyState取值描述

0
描述一种"未初始化"状态;此时,已经创建一个XMLHttpRequest对象,但是还没有初始化。

1
描述一种"发送"状态;此时,代码已经调用了XMLHttpRequest open()方法并且XMLHttpRequest已经准备好把一个请求发送到服务器。

2
描述一种"发送"状态;此时,已经通过send()方法把一个请求发送到服务器端,但是还没有收到一个响应。

3
描述一种"正在接收"状态;此时,已经接收到HTTP响应头部信息,但是消息体部分还没有完全接收结束。

4
描述一种"已加载"状态;此时,响应已经被完全接收。

  onreadystatechange事件

 
 无论readyState值何时发生改变,XMLHttpRequest对象都会激发一个readystatechange事件。其
中,onreadystatechange属性接收一个EventListener值-向该方法指示无论readyState值何时发生改变,该对象都将
激活。

  responseText属性

  这个responseText属性包含客户端接收到的HTTP响应的文本内
容。当readyState值为0、1或2时,responseText包含一个空字符串。当readyState值为3(正在接收)时,响应中包含客户
端还未完成的响应信息。当readyState为4(已加载)时,该responseText包含完整的响应信息。

  responseXML属性

 
 此responseXML属性用于当接收到完整的HTTP响应时(readyState为4)描述XML响应;此时,Content-Type头部指定
MIME(媒体)类型为text/xml,application/xml或以+xml结尾。如果Content-Type头部并不包含这些媒体类型之
一,那么responseXML的值为null。无论何时,只要readyState值不为4,那么该responseXML的值也为null。

  其实,这个responseXML属性值是一个文档接口类型的对象,用来描述被分析的文档。如果文档不能被分析(例如,如果文档不是良构的或不支持文档相应的字符编码),那么responseXML的值将为null。

  status属性

  这个status属性描述了HTTP状态代码,而且其类型为short。而且,仅当readyState值为3(正在接收中)或4(已加载)时,这个status属性才可用。当readyState的值小于3时试图存取status的值将引发一个异常。

  statusText属性

  这个statusText属性描述了HTTP状态代码文本;并且仅当readyState值为3或4才可用。当readyState为其它值时试图存取statusText属性将引发一个异常。

 三、 XMLHttpRequest对象的方法

  XMLHttpRequest对象提供了各种方法用于初始化和处理HTTP请求,下列将逐个展开详细讨论。

  abort()方法

  你可以使用这个abort()方法来暂停与一个XMLHttpRequest对象相联系的HTTP请求,从而把该对象复位到未初始化状态。

  open()方法

 
 你需要调用open(DOMString method,DOMString uri,boolean async,DOMString
username,DOMString
password)方法初始化一个XMLHttpRequest对象。其中,method参数是必须提供的-用于指定你想用来发送请求的HTTP方法
(GET,POST,PUT,DELETE或HEAD)。为了把数据发送到服务器,应该使用POST方法;为了从服务器端检索数据,应该使用GET方法。
另外,uri参数用于指定XMLHttpRequest对象把请求发送到的服务器相应的URI。借助于window.document.baseURI属
性,该uri被解析为一个绝对的URI-换句话说,你可以使用相对的URI-它将使用与浏览器解析相对的URI一样的方式被解析。async参数指定是否
请求是异步的-缺省值为true。为了发送一个同步请求,需要把这个参数设置为false。对于要求认证的服务器,你可以提供可选的用户名和口令参数。在
调用open()方法后,XMLHttpRequest对象把它的readyState属性设置为1(打开)并且把responseText、
responseXML、status和statusText属性复位到它们的初始值。另外,它还复位请求头部。注意,如果你调用open()方法并且此
时readyState为4,则XMLHttpRequest对象将复位这些值。

  send()方法

  在通过调用
open()方法准备好一个请求之后,你需要把该请求发送到服务器。仅当readyState值为1时,你才可以调用send()方法;否则的
话,XMLHttpRequest对象将引发一个异常。该请求被使用提供给open()方法的参数发送到服务器。当async参数为true
时,send()方法立即返回,从而允许其它客户端脚本处理继续。在调用send()方法后,XMLHttpRequest对象把readyState的
值设置为2(发送)。当服务器响应时,在接收消息体之前,如果存在任何消息体的话,XMLHttpRequest对象将把readyState设置为
3(正在接收中)。当请求完成加载时,它把readyState设置为4(已加载)。对于一个HEAD类型的请求,它将在把readyState值设置为
3后再立即把它设置为4。

  send()方法使用一个可选的参数-该参数可以包含可变类型的数据。典型地,你使用它并通过POST方法
把数据发送到服务器。另外,你可以显式地使用null参数调用send()方法,这与不用参数调用它一样。对于大多数其它的数据类型,在调用send()
方法之前,应该使用setRequestHeader()方法(见后面的解释)先设置Content-Type头部。如果在send(data)方法中的
data参数的类型为DOMString,那么,数据将被编码为UTF-8。如果数据是Document类型,那么将使用由
data.xmlEncoding指定的编码串行化该数据。

  setRequestHeader()方法

  该setRequestHeader(DOMString header,DOMString value)方法用来设置请求的头部信息。当readyState值为1时,你可以在调用open()方法后调用这个方法;否则,你将得到一个异常。

  getResponseHeader()方法

  getResponseHeader(DOMString header,value)方法用于检索响应的头部值。仅当readyState值是3或4(换句话说,在响应头部可用以后)时,才可以调用这个方法;否则,该方法返回一个空字符串。

  getAllResponseHeaders()方法

  该getAllResponseHeaders()方法以一个字符串形式返回所有的响应头部(每一个头部占单独的一行)。如果readyState的值不是3或4,则该方法返回null。

  四、 发送请求

  在AJAX中,许多使用XMLHttpRequest的请求都是从一个HTML事件(例如一个调用
JavaScript函数的按钮点击(onclick)或一个按键(onkeypress))中被初始化的。AJAX支持包括表单校验在内的各种应用程
序。有时,在填充表单的其它内容之前要求校验一个唯一的表单域。例如要求使用一个唯一的UserID来注册表单。如果不是使用AJAX技术来校验这个
UserID域,那么整个表单都必须被填充和提交。如果该UserID不是有效的,这个表单必须被重新提交。例如,一个相应于一个要求必须在服务器端进行
校验的Catalog ID的表单域可能按下列形式指定:





<form name="validationForm" action="validateForm" method="post">
<table>
 <tr><td>Catalog Id:</td>
  <td>
   <input type="text" size="20" id="catalogId" name="catalogId" autocomplete="off" onkeyup="sendRequest()">
  </td>
  <td><div id="validationMessage"></div></td>
 </tr>
</table></form>

 
 前面的HTML使用validationMessage div来显示相应于这个输入域Catalog
Id的一个校验消息。onkeyup事件调用一个JavaScript
sendRequest()函数。这个sendRequest()函数创建一个XMLHttpRequest对象。创建一个XMLHttpRequest
对象的过程因浏览器实现的不同而有所区别。如果浏览器支持XMLHttpRequest对象作为一个窗口属性(所有普通的浏览器都是这样的,除了IE
5和IE
6之外),那么,代码可以调用XMLHttpRequest的构造器。如果浏览器把XMLHttpRequest对象实现为一个
ActiveXObject对象(就象在IE 5和IE
6中一样),那么,代码可以使用ActiveXObject的构造器。下面的函数将调用一个init()函数,它负责检查并决定要使用的适当的创建方法-
在创建和返回对象之前。





<script type="text/javascript">
function sendRequest(){
 var xmlHttpReq=init();
 function init(){
  if (window.XMLHttpRequest) {
   return new XMLHttpRequest();
  }
 else if (window.ActiveXObject) {
  return new ActiveXObject("Microsoft.XMLHTTP");
 }
}
</script>

  接下来,你需要使用Open()方法初始化XMLHttpRequest对象-指定HTTP方法和要使用的服务器URL。





var catalogId=encodeURIComponent(document.getElementById("catalogId").value);
xmlHttpReq.open("GET", "validateForm?catalogId=" + catalogId, true);

  默认情况下,使用XMLHttpRequest发送的HTTP请求是异步进行的,但是你可以显式地把async参数设置为true,如上面所展示的。

这种情况下,对URL
validateForm的调用将激活服务器端的一个servlet,但是你应该能够注意到服务器端技术不是根本性的;实际上,该URL可能是一个
ASP,ASP.NET或PHP页面或一个Web服务-这无关紧要,只要该页面能够返回一个响应-指示CatalogID值是否是有效的-即可。因为你在
作一个异步调用,所以你需要注册一个XMLHttpRequest对象将调用的回调事件处理器-当它的readyState值改变时调用。记
住,readyState值的改变将会激发一个readystatechange事件。你可以使用onreadystatechange属性来注册该回调
事件处理器。





xmlHttpReq.onreadystatechange=processRequest;

  然后,我们需要使用send()方法发送该请求。因为这个请求使用的是HTTP GET方法,所以,你可以在不指定参数或使用null参数的情况下调用send()方法。





xmlHttpReq.send(null);


  五、 处理请求

  在这个示例中,因为HTTP方法是GET,所以在服务器端的接收servlet将调用一个doGet()方法,该方法将检索在URL中指定的catalogId参数值,并且从一个数据库中检查它的有效性。

 
 本文示例中的这个servlet需要构造一个发送到客户端的响应;而且,这个示例返回的是XML类型,因此,它把响应的HTTP内容类型设置为
text/xml并且把Cache-Control头部设置为no-cache。设置Cache-Control头部可以阻止浏览器简单地从缓存中重载页
面。





public void doGet(HttpServletRequest request,
HttpServletResponse response)
throws ServletException, IOException {
 ...
 ...
 response.setContentType("text/xml");
 response.setHeader("Cache-Control", "no-cache");
}

  来自于服务器端的响应是一个XML DOM对象,此对象将创建一个XML字符串-其中包含要在客户端进行处理的指令。另外,该XML字符串必须有一个根元素。





out.println("<catalogId>valid</catalogId>");

  【注意】XMLHttpRequest对象的设计目的是为了处理由普通文本或XML组成的响应;但是,一个响应也可能是另外一种类型,如果用户代理(UA)支持这种内容类型的话。

 
 当请求状态改变时,XMLHttpRequest对象调用使用onreadystatechange注册的事件处理器。因此,在处理该响应之前,你的事
件处理器应该首先检查readyState的值和HTTP状态。当请求完成加载(readyState值为4)并且响应已经完成(HTTP状态
为"OK")时,你就可以调用一个JavaScript函数来处理该响应内容。下列脚本负责在响应完成时检查相应的值并调用一个
processResponse()方法。





function processRequest(){
 if(xmlHttpReq.readyState==4){
  if(xmlHttpReq.status==200){
   processResponse();
  }
 }
}

 
 该processResponse()方法使用XMLHttpRequest对象的responseXML和responseText属性来检索
HTTP响应。如上面所解释的,仅当在响应的媒体类型是text/xml,application/xml或以+xml结尾时,这个
responseXML才可用。这个responseText属性将以普通文本形式返回响应。对于一个XML响应,你将按如下方式检索内容:





var msg=xmlHttpReq.responseXML;

  借助于存储在msg变量中的XML,你可以使用DOM方法getElementsByTagName()来检索该元素的值:





var catalogId=msg.getElementsByTagName("catalogId")[0].firstChild.nodeValue;

  最后,通过更新Web页面的validationMessage div中的HTML内容并借助于innerHTML属性,你可以测试该元素值以创建一个要显示的消息:





if(catalogId=="valid"){
 var validationMessage = document.getElementById("validationMessage");
 validationMessage.innerHTML = "Catalog Id is Valid";
}
else
{
 var validationMessage = document.getElementById("validationMessage");
 validationMessage.innerHTML = "Catalog Id is not Valid";
}

  六、 小结

 
 上面就是XMLHttpRequest对象使用的所有细节实现。通过不必把Web页面寄送到服务器而实现数据传送,XMLHttpRequest对象为
客户端与服务器之间提供了一种动态的交互能力。你可以使用JavaScript启动一个请求并处理相应的返回值,然后使用浏览器的DOM方法更新页面中的
数据。







2009年4月16日星期四

用LVS构架负载均衡Linux集群系统


Redhat Linux ES 5.0下面安装:
1. 因为2.6内核已经集成IPVS内核补订了,所以不再需要重新编译内核.
2. 下载ipvsadm-1.24-6.src.rpm放到某个目录
3. root用户执行 ln -s /usr/src/kernels/2.6.18-53.el5-xen-i686 /usr/src/linux,把内核连接到/usr/src/linux目录
4. rpm -iph ipvsadm-1.24-6.src.rpm,解压安装ipvs代码包
5. 修改/usr/src/redhat/SPECS下面的ipvsadm.spec文件,把这一行Copyright: GNU General Public Licence 替换成 License: GNU General Public Licence
6. 执行 rpmbuild -ba /usr/src/redhat/SPECS/ipvsadm.spec,等待build完成
7. 最后执行rpm -ivh /usr/src/redhat/RPMS/i386/ipvsadm-1.24-6.i386.rpm 安装刚刚编译的包
8. 执行ipvsadm检查是否安装成功。

DR直连方式实例:
Director物理地址: 192.168.145.128
RealServer1物理地址: 192.168.145.129
RealServer2物理地址: 192.168.145.130
虚拟地址:192.168.145.132,端口都是80,httpd服务。
只要在三个服务器上运行相应的脚本,就可以实现LVS服务:
Director:
[root@localhost LVS]# cat lvs_dr.sh
#/bin/bash
# set ip_forward OFF for vs-dr director (1 on, 0 off)
echo 0 > /proc/sys/net/ipv4/ip_forward
ifconfig  eth0:0 192.168.145.132 netmask 255.255.255.255 broadcast 192.168.145.132 up
/sbin/route add -host 192.168.145.132 dev eth0:0
ipvsadm -C
ipvsadm -A -t 192.168.145.132:80 -s rr
# Set Real Server
ipvsadm -a -t 192.168.145.132:80 -r 192.168.145.129:80 -g
ipvsadm -a -t 192.168.145.132:80 -r 192.168.145.130:80 -g
ipvsadm

Real Server后台服务器:
[root@localhost LVS]# cat realserver
#!/bin/sh
VIP=192.168.145.132
/sbin/ifconfig lo:0 $VIP broadcast $VIP netmask 255.255.255.255 up
/sbin/route add -host $VIP dev lo:0
echo "1" > /proc/sys/net/ipv4/conf/lo/arp_ignore
echo "2" > /proc/sys/net/ipv4/conf/lo/arp_announce
echo "1" > /proc/sys/net/ipv4/conf/all/arp_ignore
echo "2" > /proc/sys/net/ipv4/conf/all/arp_announce
sysctl -p













下面看看lvs控制台的基本命令


添加一个Service



  1. # ipvsadm -A -t 192.168.8.100:80 -s rr      
  2. rr:表示轮询的方法,缺省为wcl  


添加一个realserver



  1. # ipvsadm -a -t 192.168.8.100:80 -r 192.168.8.180:80 -g    
  2. -a:添加一个realserver   
  3. -r:realserver的地址   
  4. -g:缺省参数   


修改realserver



  1. # ipvsadm -e -t 192.168.8.100:80 -r 192.168.8.180:80 -w 100   
  2. -e:修改-r参数的realserver   
  3. -w:lvs转发通道的处理能力 














 用LVS构架负载均衡Linux集群系统


最近有客户需要一个负载均衡方
案,笔者对各种软硬件的负载均衡方案进行了调查和比较,从IBM sServer Cluster、Sun Cluster
PlatForm等硬件集群,到中软、红旗、TurboLinux的软件集群,发现无论采用哪个厂商的负载均衡产品其价格都是该客户目前所不能接受的。于
是笔者想到了开放源项目Linux Virtual Server(简称LVS)。经过对LVS的研究和实验,终于在Red Hat
9.0上用LVS成功地构架了一组负载均衡的集群系统。整个实现过程整理收录如下,供读者参考。

选用的LVS实际上是一种Linux操作系统上基于IP层的负载均衡调度技术,它
在操作系统核心层上,将来自IP层的TCP/UDP请求均衡地转移到不同的服务器,从而将一组服务器构成一个高性能、高可用的虚拟服务器。使用三台机器就
可以用LVS实现最简单的集群,如图1所示。

图1显示一台名为Director的机器在集群前端做负载分配工作;后端两台机器
称之为Real
Server,专门负责处理Director分配来的外界请求。该集群的核心是前端的Director机器,LVS就是安装在这台机器上,它必须安装
Linux。Real Server则要根据其选用的负载分配方式而定,通常Real
Server上的设置比较少。接下来介绍Director机器上LVS的安装过程。


图1 LVS实现集群系统结构简图


安装

LVS的安装主要是在Director机器上进行,Real Server只需针对不同的转发方式做简单的设定即可。特别是对LVS的NAT方式,Real Server惟一要做的就是设一下缺省的网关。所以构架集群的第一步从安装Director机器开始。

首先要在Director机器上安装一个Linux操作系统。虽然早期的一些Red
Hat版本,如6.2、7.2、8.0等自带Red
Hat自己的集群软件,或者是在内核中已经支持LVS,但是为了更清楚地了解LVS的机制,笔者还是选择自行将LVS编入Linux内核的方式进行安
装,Linux版本采用Red Hat 9.0。

如果用户对Red Hat的安装比较了解,可以选择定制安装,并只安装必要的软件包。安装中请选择GRUB做为启动引导管理软件。因为GRUB在系统引导方面的功能远比LILO强大,在编译Linux内核时可以体会它的方便之处。

LVS是在Linux内核中实现的,所以要对原有的Linux内核打上支持LVS的
内核补丁,然后重新编译内核。支持LVS的内核补丁可以从LVS的官方网站http://www.linuxvirtualserver.org下载,下
载时请注意使用的Linux核心版本,必须下载和使用的Linux内核版本相一致的LVS内核补丁才行。对于Red Hat
9.0,其Linux内核版本是2.4.20,所以对应内核补丁应该是http://www.linuxvirtualserver.org
/software/kernel-2.4/linux-2.4.20-ipvs-1.0.9.patch.gz。笔者经过多次实验,使用Red
Hat
9.0自带的Linux源代码无法成功编译LVS的相关模组。由于时间关系笔者没有仔细研究,而是另外从kernel.org上下载了一个tar包格式的
2.4.20内核来进行安装,顺利完成所有编译。下面是整个内核的编译过程:

1.删除Red Hat自带的Linux源代码







# cd /usr/src
# rm -rf linux*



2.下载2.4.20内核







# cd /usr/src
# wget ftp://ftp.kernel.org/pub/linux/kernel/v2.4/linux-2.4.20.tar.bz2



3.解压到当前目录/usr/src







# cd /usr/src
# tar -xjpvf linux-2.4.20.tar.bz2



4.建立链接文件







# cd /usr/src
# ln -s linux-2.4.20 linux-2.4
# ln -s linux-2.4.20 linux



5.打上LVS的内核补丁







# cd /usr/src
# wget http://www.linuxvirtualserver.org/software/kernel-2.4/linux-2.4.20-ipvs-1.0.9.patch.gz
# gzip -cd linux-2.4.20-ipvs-1.0.9.patch.gz
# cd /usr/src/linux
# patch -p1 < ../linux-2.4.20-ipvs-1.0.9.patch



在打补丁时,注意命令执行后的信息,不能有任何错误信息,否则核心或模组很可能无法成功编译。

6.打上修正ARP问题的内核补丁







# cd /usr/src
# wget http://www.ssi.bg/~ja/hidden-2.4.20pre10-1.diff
# cd /usr/src/linux
# patch -p1 < ../hidden-2.4.20pre10-1.diff



这一步在Director机器上可以不做,但是在使用LVS/TUN和LVS/DR方式的Real Server上必须做。

7.为新核心命名

打开/usr/src/linux/Makefile。注意,在开始部分有一个变量
EXTRAVERSION可以自行定义。修改这个变量,比如改成“EXTRAVERSION=-LVS”后,编译出的核心版本号就会显示成2.4.20-
LVS。这样给出有含义的名称将有助于管理多个Linux核心。

8.检查源代码







# make mrproper



这一步是为确保源代码目录下没有不正确的.o文件及文件的互相依赖。因为是新下载的内核,所以在第一次编译时,这一步实际可以省略。

9.配置核心选项







# make menuconfig



命令执行后会进入一个图形化的配置界面,可以通过这个友好的图形界面对内核进行定
制。此过程中,要注意对硬件驱动的选择。Linux支持丰富的硬件,但对于服务器而言,用不到的硬件驱动都可以删除。另外,像Multimedia
devices、Sound、Bluetooth support、Amateur Radio support等项也可以删除。

注意,以下几项配置对LVS非常重要,请确保作出正确的选择:

(1)Code maturity level options项

对此项只有以下一个子选项,请选中为*,即编译到内核中去。







[*]Prompt for development and/or incomplete code/drivers



(2)Networking options项

对此项的选择可以参考以下的配置,如果不清楚含义可以查看帮助:







<*> Packet socket
[ ] Packet socket: mmapped IO
< > Netlink device emulation
[*] Network packet filtering (replaces ipchains)
[ ] Network packet filtering debugging
[*] Socket Filtering
<*> Unix domain sockets
[*] TCP/IP networking
[*] IP: multicasting
[*] IP: advanced router
[*] IP: policy routing
[ ] IP: use netfilter MARK value as routing key
[ ] IP: fast network address translation
<M> IP: tunneling
[*] IP: broadcast GRE over IP
[ ] IP: multicast routing
[ ] IP: ARP daemon support (EXPERIMENTAL)
[ ] IP: TCP Explicit Congestion Notification support
[ ] IP: TCP syncookie support (disabled per default)
IP: Netfilter Configuration --->
IP: Virtual Server Configuration --->



(3)Networking options项中的IP: Virtual Server Configuration项

如果打好了LVS的内核补丁,就会出现此选项。进入Virtual Server Configuration选项,有以下子选项:







<M> virtual server support (EXPERIMENTAL)      
[*] IP virtual server debugging
(12) IPVS connection table size (the Nth power of 2)
--- IPVS scheduler
<M> round-robin scheduling
<M> weighted round-robin scheduling
<M> least-connection scheduling scheduling
<M> weighted least-connection scheduling
<M> locality-based least-connection scheduling
<M> locality-based least-connection with replication scheduling
<M> destination hashing scheduling
<M> source hashing scheduling
<M> shortest expected delay scheduling
<M> never queue scheduling
--- IPVS application helper
<M> FTP protocol helper



以上所有项建议全部选择。

(4)Networking options项中的IP: Netfilter Configuration项

对于2.4版本以上的Linux
Kernel来说,iptables是取代早期ipfwadm和ipchains的更好选择,所以除非有特殊情况需要用到对ipchains和
ipfwadm的支持,否则就不要选它。本文在LVS/NAT方式中,使用的就是iptables,故这里不选择对ipchains和ipfwadm的支
持:





< > ipchains (2.2-style) support                
< > ipfwadm (2.0-style) support



10. 编译内核

(1)检查依赖关系







# make dep



确保关键文件在正确的路径上。

(2)清除中间文件







# make clean



确保所有文件都处于最新的版本状态下。

(3)编译新核心







# make bzImage



(4)编译模组







# make modules



编译选择的模组。

(5)安装模组







# make modules_install
# depmod -a



生成模组间的依赖关系,以便modprobe定位。

(6)使用新模组







# cp System.map  /boot/System.map-2.4.20-LVS
# rm /boot/System.map
# ln -s /boot/System.map-2.4.20-LVS /boot/System.map
# cp arch/i386/boot/bzImage /boot/vmlinuz-2.4.20-LVS
# rm /boot/vmlinuz
# ln -s /boot/vmlinuz-2.4.20-LVS /boot/vmlinuz
# new-kernel-pkg --install --mkinitrd --depmod 2.4.20-LVS



(7)修改GRUB,以新的核心启动

执行完new-kernel-pkg命令后,GRUB的设置文件/etc/grub.conf中已经增加了新核心的启动项,这正是开始安装Linux时推荐使用GRUB做引导程序的原因。







grub.conf中新增内容如下:
title Red Hat Linux (2.4.20-LVS)
root (hd0,0)
kernel /boot/vmlinuz-2.4.20LVS ro root=LABEL=/
initrd /boot/initrd-2.4.20LVS.img



将Kernel项中的root=LABEL=/改成 root=/dev/sda1 (这里的/dev/sda1是笔者Linux的根分区,读者可根据自己的情况进行不同设置)。

保存修改后,重新启动系统:







# reboot



系统启动后,在GRUB的界面上会出现Red Hat
Linux(2.4.20-LVS)项。这就是刚才编译的支持LVS的新核心,选择此项启动,看看启动过程是否有错误发生。如果正常启动,ipvs将作为
模块加载。同时应该注意到,用LVS的内核启动后在/proc目录中新增了一些文件,比如/proc/sys/net/ipv4/vs/*。

11.安装IP虚拟服务器软件ipvsadm

用支持LVS的内核启动后,即可安装IP虚拟服务器软件ipvsadm了。用户可以
用tar包或RPM包安装,tar包可以从以下地址http://www.linuxvirtualserver.org/software
/kernel-2.4/ipvsadm-1.21.tar.gz下载进行安装。

这里采用源RPM包来进行安装:







# wget http://www.linuxvirtualserver.org/software/kernel-2.4/ipvsadm-1.21-7.src.rpm
# rpmbuild --rebuild ipvsadm-1.21-7.src.rpm
# rpm -ivh /usr/src/redhat/RPMS/i386/ipvsadm-1.21-7.i386.rpm



注意高版本的rpm命令去掉了--rebuild这个参数选项,但提供了一个rpmbuild命令来实现它。这一点和以前在Red Hat 6.2中以rpm—rebuild XXX.src.rpm来安装源RPM包的习惯做法有所不同。

安装完,执行ipvsadm命令,应该有类似如下的信息出现:







# ipvsadm
IP Virtual Server version 1.0.9 (size=4096)
Prot LocalAddress:Port Scheduler Flags
-> RemoteAddress:Port Forward Weight ActiveConn InActConn



出现类似以上信息,表明支持LVS的内核和配置工具ipvsadm已完全安装,这台Director机器已经初步安装完成,已具备构架各种方式的集群的条件。

原理

接下来的工作是根据实际需求选择采用何种负载分配方式和调度算法。目前LVS主要有
三种请求转发方式和八种调度算法。根据请求转发方式的不同,所构架集群的网络拓扑、安装方式、性能表现也各不相同。用LVS主要可以架构三种形式的集群,
分别是LVS/NAT、LVS/TUN和LVS/DR,可以根据需要选择其中一种。在选定转发方式的情况下,采用哪种调度算法将决定整个负载均衡的性能表
现,不同的算法适用于不同的应用场合,有时可能需要针对特殊场合,自行设计调度算法。LVS的算法是逐渐丰富起来的,最初LVS只提供4种调度算法,后来
发展到以下八种:

1.轮叫调度(Round Robin)

调度器通过“轮叫”调度算法将外部请求按顺序轮流分配到集群中的真实服务器上,它均等地对待每一台服务器,而不管服务器上实际的连接数和系统负载。

2.加权轮叫(Weighted Round Robin)

调度器通过“加权轮叫”调度算法根据真实服务器的不同处理能力来调度访问请求。这样可以保证处理能力强的服务器能处理更多的访问流量。调度器可以自动问询真实服务器的负载情况,并动态地调整其权值。

3.最少链接(Least Connections)

调度器通过“最少连接”调度算法动态地将网络请求调度到已建立的链接数最少的服务器上。如果集群系统的真实服务器具有相近的系统性能,采用“最小连接”调度算法可以较好地均衡负载。

4.加权最少链接(Weighted Least Connections)

在集群系统中的服务器性能差异较大的情况下,调度器采用“加权最少链接”调度算法优化负载均衡性能,具有较高权值的服务器将承受较大比例的活动连接负载。调度器可以自动问询真实服务器的负载情况,并动态地调整其权值。

5.基于局部性的最少链接(Locality-Based Least Connections)

“基于局部性的最少链接”调度算法是针对目标IP地址的负载均衡,目前主要用于
Cache集群系统。该算法根据请求的目标IP地址找出该目标IP地址最近使用的服务器,若该服务器是可用的且没有超载,将请求发送到该服务器;若服务器
不存在,或者该服务器超载且有服务器处于一半的工作负载,则用“最少链接”的原则选出一个可用的服务器,将请求发送到该服务器。

6.带复制的基于局部性最少链接(Locality-Based Least Connections with Replication)

“带复制的基于局部性最少链接”调度算法也是针对目标IP地址的负载均衡,目前主要
用于Cache集群系统。它与LBLC算法的不同之处是它要维护从一个目标IP地址到一组服务器的映射,而LBLC算法维护从一个目标IP地址到一台服务
器的映射。该算法根据请求的目标IP地址找出该目标IP地址对应的服务器组,按“最小连接”原则从服务器组中选出一台服务器,若服务器没有超载,将请求发
送到该服务器;若服务器超载,则按“最小连接”原则从这个集群中选出一台服务器,将该服务器加入到服务器组中,将请求发送到该服务器。同时,当该服务器组
有一段时间没有被修改,将最忙的服务器从服务器组中删除,以降低复制的程度。

7.目标地址散列(Destination Hashing)

“目标地址散列”调度算法根据请求的目标IP地址,作为散列键(Hash Key)从静态分配的散列表找出对应的服务器,若该服务器是可用的且未超载,将请求发送到该服务器,否则返回空。

8.源地址散列(Source Hashing)

“源地址散列”调度算法根据请求的源IP地址,作为散列键(Hash Key)从静态分配的散列表找出对应的服务器,若该服务器是可用的且未超载,将请求发送到该服务器,否则返回空。

了解这些算法原理能够在特定的应用场合选择最适合的调度算法,从而尽可能地保持Real Server的最佳利用性。当然也可以自行开发算法,不过这已超出本文范围,请参考有关算法原理的资料。



实例

理解了上述关于请求转发方式和调度算法的基本概念后,就可以运用LVS来具体实现几
种不同方式的负载均衡的集群系统。LVS的配置是通过前面所安装的IP虚拟服务器软件ipvsadm来实现的。ipvsadm与LVS的关系类似于
iptables和NetFilter的关系,前者只是一个建立和修改规则的工具,这些命令的作用在系统重新启动后就消失了,所以应该将这些命令写到一个
脚本里,然后让它在系统启动后自动执行。网上有不少配置LVS的工具,有的甚至可以自动生成脚本。但是自己手工编写有助于更深入地了解,所以本文的安装没
有利用其它第三方提供的脚本,而是纯粹使用ipvsadm命令来配置。

下面就介绍一下如何配置LVS/NAT、LVS/TUN、LVS/DR方式的负载均衡集群。

1.设定LVS/NAT方式的负载均衡集群

NAT是指Network Address
Translation,它的转发流程是:Director机器收到外界请求,改写数据包的目标地址,按相应的调度算法将其发送到相应Real
Server上,Real
Server处理完该请求后,将结果数据包返回到其默认网关,即Director机器上,Director机器再改写数据包的源地址,最后将其返回给外
界。这样就完成一次负载调度。

构架一个最简单的LVS/NAT方式的负载均衡集群如图2所示。


图2 LVS/NAT方式的集群简图


Real
Server可以是任何的操作系统,而且无需做任何特殊的设定,惟一要做的就是将其默认网关指向Director机器。Real
Server可以使用局域网的内部IP(192.168.0.0/24)。Director要有两块网卡,一块网卡绑定一个外部IP地址
(10.0.0.1),另一块网卡绑定局域网的内部IP(192.168.0.254),作为Real Server的默认网关。

这里将所有LVS的配置命令写到一个可执行脚本中,脚本如下:







#!/bin/bash
# Open IP Forwarding
echo 1 > /proc/sys/net/ipv4/ip_forward
# To make the load balancer forward the masquerading packets
iptables -t nat -A POSTROUTING -s 192.168.2.0/24 -d 0.0.0.0/0 -o eth0 -j MASQUERADE
ipvsadm -C
# Choose the Weighted Round Robing
ipvsadm -A -t 10.0.0.1:80 -s wrr
# Set Real Server
ipvsadm -a -t 10.0.0.1:80 -r 192.168.0.1:873 -m -w 2
ipvsadm -a -t 10.0.0.1:80 -r 192.168.0.2:873 -m -w 3
ipvsadm



将该脚本保存为/root/lvs_nat.sh,然后加上可执行属性,执行它:







# chmod a+x /root/lvs_nat.sh
# /root/lvs_nat.sh



运行该脚本后,一个简单的LVS/NAT方式的负载均衡集群已经成功架设。模拟多个用户从外界访问10.0.0.1的80端口,用ipvsadm可以观看到以下信息:







# ipvsadm
IP Virtual Server version 1.0.9 (size=4096)
Prot LocalAddress:Port Scheduler Flags
-> RemoteAddress:Port Forward Weight ActiveConn InActConn
TCP 10.0.0.1:http wrr
-> 192.168.0.1:http Masq 3 2 0
-> 192.168.0.2:http Masq 2 1 0



其中ActiveConn表示对应的Real Server当前有多少个正在活动的连接,InActConn表示不活动的连接数。从这里我们可以看到有3个HTTP请求,被分别分配在不同的Real Server上,表明这个负载均衡集群正在成功运行中。

本例完成了这样一个简单的LVS/NAT集群,由此可以看出,LVS/NAT方式实
现起来最为简单,而且Real Server使用的是内部IP,可以节省Real
IP的开销。但因为执行NAT需要重写流经Director的数据包,在速度上有一定延迟;另外,当用户的请求非常短,而服务器的回应非常大的情况下,会
对Director形成很大压力,成为新的瓶颈,从而使整个系统的性能受到限制。

2.设定LVS/TUN方式的负载均衡集群

TUN是指IP Tunneling,它的转发流程是:Director机器收到外界请求,按相应的调度算法将其通过IP隧道发送到相应Real Server,Real Server处理完该请求后,将结果数据包直接返回给客户。至此完成一次负载调度。

最简单的LVS/TUN方式的负载均衡集群架构如图3所示。

LVS/TUN使用IP
Tunneling技术,在Director机器和Real Server机器之间架设一个IP Tunnel,通过IP
Tunnel将负载分配到Real Server机器上。Director和Real
Server之间的关系比较松散,可以是在同一个网络中,也可以是在不同的网络中,只要两者能够通过IP
Tunnel相连就行。收到负载分配的Real
Server机器处理完后会直接将反馈数据送回给客户,而不必通过Director机器。实际应用中,服务器必须拥有正式的IP地址用于与客户机直接通
信,并且所有服务器必须支持IP隧道协议。


图3 LVS/TUN方式的集群简图


这里将所有LVS的配置命令写到一个可执行脚本,脚本内容如下:







#!/bin/bash
# Close IP Forwarding
echo 0 > /proc/sys/net/ipv4/ip_forward
ifconfig eth0 down
ifconfig eth0 192.168.0.253 netmask 255.255.255.0 broadcast 192.168.0.255 up
ifconfig eth0:0 192.168.0.254 netmask 255.255.255.255 broadcast 192.168.0.254 up
ipvsadm -C
ipvsadm -A -t 192.168.0.254:80 -s wlc
ipvsadm -a -t 192.168.0.254:80 -r 192.168.0.1 -i -w 3
ipvsadm -a -t 192.168.0.254:80 -r 192.168.1.201 -i -w 1
ipvsadm



将上面的脚本保存为/root/lvs_tun.sh。然后加上可执行属性,执行它:







# chmod a+x /root/lvs_tun.sh
# /root/lvs_tun.sh



运行此脚本之后应该出现如下信息:







# ./lvs-tun.sh
IP Virtual Server version 1.0.9 (size=4096)
Prot LocalAddress:Port Scheduler Flags
-> RemoteAddress:Port Forward Weight ActiveConn InActConn
TCP 192.168.0.254:http wlc
-> 192.168.1.201:http Tunnel 1 0 0
-> 192.168.0.1:http Tunnel 1 0 0



另外在每台Real Server上还要执行如下的命令:







ifconfig tunl0 192.168.0.254 netmask 255.255.255.255 broadcast 192.168.0.254 up
route add -host 192.168.0.254 dev tunl0
echo 1 > /proc/sys/net/ipv4/conf/all/hidden
echo 1 > /proc/sys/net/ipv4/conf/tunl0/hidden



注意Real Server的内核必须打上修正ARP问题的内核补丁,如Linux2.4.20的内核是hidden-2.4.20pre10-1.diff,编译内核的方法参见Director机器。

通过本例来简单评价一下LVS/TUN方式。该方式中Director将客户请求分
配到不同的Real Server,Real
Server处理请求后直接回应给用户,这样Director就只处理客户机与服务器的一半连接,极大地提高了Director的调度处理能力,使集群系
统能容纳更多的节点数。另外TUN方式中的Real
Server可以在任何LAN或WAN上运行,这样可以构筑跨地域的集群,其应对灾难的能力也更强,但是服务器需要为IP封装付出一定的资源开销,而且后
端的Real Server必须是支持IP Tunneling的操作系统。

3.设定LVS/DR方式的负载均衡集群

DR是指Direct Routing,它的转发流程是:Director机器收到外界请求,按相应的调度算法将其直接发送到相应Real Server,Real Server处理完该请求后,将结果数据包直接返回给客户,完成一次负载调度。

构架一个最简单的LVS/DR方式的负载均衡集群如图4所示。

Real
Server和Director都在同一个物理网段中,Director的网卡IP是192.168.0.253,再绑定另一个
IP:192.168.0.254作为对外界的virtual IP,外界客户通过该IP来访问整个集群系统。Real
Server在lo上绑定IP:192.168.0.254,同时加入相应的路由。

Director端的实现脚本如下:







#!/bin/bash
# set ip_forward OFF for vs-dr director (1 on, 0 off)
echo 0 > /proc/sys/net/ipv4/ip_forward
ifconfig eth0:0 192.168.0.254 netmask 255.255.255.255 broadcast 192.168.0.255 up
ipvsadm -C
ipvsadm -A -t 192.168.0.254:80 -s wlc
# Set Real Server
ipvsadm -a -t 192.168.0.254:80 -r 192.168.0.1:873 -g
ipvsadm -a -t 192.168.0.254:80 -r 192.168.0.2:873 -g
ipvsadm




图4 LVS/DR方式的集群简图


将脚本保存为/root/lvs_dr.sh,加上可执行属性,执行它:







# chmod a+x /root/lvs_dr.sh
# /root/lvs_dr.sh



运行此脚本之后可以看到如下信息:







# ./lvs_dr.sh
IP Virtual Server version 1.0.9 (size=4096)
Prot LocalAddress:Port Scheduler Flags
-> RemoteAddress:Port Forward Weight ActiveConn InActConn
TCP 192.168.0.254:http wlc
-> 192.168.0.2:http Route 1 0 0
-> 192.168.0.1:http Route 1 0 0



另外每台Real Server上要执行如下命令:







ifconfig lo:0 192.168.0.254 netmask 255.255.255.255 broadcast 192.168.0.255 up
route add -host 192.168.0.254 dev lo:0
echo 1 > /proc/sys/net/ipv4/conf/all/hidden
echo 1 > /proc/sys/net/ipv4/conf/lo/hidden



注意Real Server的内核也必须打上修正ARP问题的内核补丁,编译内核的方法参见Director机器。

同样通过本例来简单评价一下LVS/DR方式。LVS/DR方式与前面的LVS
/TUN方式有些类似,前台的Director机器也是只需要接收和调度外界的请求,而不需要负责返回这些请求的反馈结果,所以能够负载更多的Real
Server,提高Director的调度处理能力,使集群系统容纳更多的Real
Server。但LVS/DR需要改写请求报文的MAC地址,所以所有服务器必须在同一物理网段内。

结束语

其实集群架设到此还不能达到正式应用的要求,至少还需要解决三个问题:

1. 安装监视软件

集群运作时,应当监视集群中所有Real
Server的运行情况并对其中的变化作出反应。如果发现Real
Server突然down机,需要将其从集群队列中删除,等恢复后再重新加入。mon就是这样一个系统资源监控程序,可以监控网络服务可用性、服务器问题
等。用户可以对其进行定制和扩展。

2. 后台Real Server节点的数据一致性问题和更新方法

比如一个提供Web服务的集群系统,每台服务器的Web资料必须一致,如果对Web
资料的内容进行更新、增加或删除,如何使所有服务器之间数据同步呢?对于这种数据同步问题,一是采用网络镜像,如借用Mirror、FTP等来自行编写脚
本进行镜像;另一种是使用网络文件系统,如coda等。

3. 提高可用性

以上示例中,Director机器只有一台,一旦Director机器down掉,整个集群也就崩溃了,所以要考虑使用两台机器做一个HA(High-Availability)。比如,配合使用Linux的另一个软件heartbeat来实现HA。

实际运行中,面对的问题可能不只以上这些,相关内容在此就不再详述,欢迎大家共同探讨。

(责任编辑:赵纪雷)





















VMware5安装集群LVS实战解析


环境描述:本文在配置LVS时使用三台linux,虚拟VIP:192.168.8.11


 


一台做Directorserver(192.168.8.2) ,操作系统RedhatAS4


 


两台做realserver(192.168.8.5,192.168.8.6) 操作系统RedhatAS4


 


在配置lvs+heartbeat时,又添加了一台(192.168.8.3)做为备份主节点, 操作系统Fedora7


 


Virtual IP: 192.168.8.11


Load Balancer: 192.168.8.2


 


Backup: 192.168.8.3


 


Real Server 1: 192.168.8.5


Real Server 2: 192.168.8.6


 


软件列表:


ipvsadm-1.24.tar.gz: http://www.linuxvirtualserver.org/software/kernel-2.6/ipvsadm-1.24.tar.gz


 


ipvsadm-1.24-8.1.i386.rpm


 


ftp://rpmfind.net/linux/fedora/releases/7/Everything/i386/os/Fedora/ipvsadm-1.24-8.1.i386.rpm


libnet.tar 下载地址:http://www.packetfactory.net/libnet/ 稳定版本是:1.1.2.1


 


heartbeat-2.0.2.tar.gz: http://linux-ha.org/download/heartbeat-2.0.8.tar.gz


 


2.6内核已经集成IPVS内核补订了,所以不再需要重新编译内核.


 


配置此集群分以下几种情况


 


一、配置基于DR模式Lvs集群


 


1、下载ipvsadm管理程序


http://www.linuxvirtualserver.org/software/


注意对应自己的内核版本


ipvsadm-1.24.tar.gz


tar zxvf ipvsadm-1.24.tar.gz


cd ipvsadm-1.24


make && make install


注意在make时可能会出现很多错误的信息,请按照如下操作就可以心编译正常


ln -s /usr/src/kernels/2.6.9-22.EL-i686/ /usr/src/linux


 


cd ipvsadm-1.24


 


make && make install


 


2.配置VIP脚本


 


[root@test7 chang]#vi LvsDR


 


#!/bin/sh


 


VIP=192.168.8.11


 


RIP1=192.168.8.6


 


RIP2=192.168.8.5


 


/etc/rc.d/init.d/functions


 


case "$1" in


 


start)


 


echo "start LVS of DirectorServer"


#Set the Virtual IP Address


 


/sbin/ifconfig eth0:1 $VIP broadcast $VIP netmask 255.255.255.255 up


 


/sbin/route add -host $VIP dev eth0:1


 


#Clear IPVS Table


 


/sbin/ipvsadm -C


 


#Set Lvs


 


/sbin/ipvsadm -A -t $VIP:80 -s rr


 


/sbin/ipvsadm -a -t $VIP:80 -r $RIP1:80 -g


 


/sbin/ipvsadm -a -t $VIP:80 -r $RIP2:80 -g


 


#Run Lvs


 


/sbin/ipvsadm


 


;;


 


stop)


 


echo "close LVS Directorserver"


 


/sbin/ipvsadm -C


 


;;


 


*)


 


echo "Usage: $0 {start|stop}"


 


exit 1


 


esac


 


(-s rr 是使用了轮叫算法,可以自行选择相应的算法,默认是-wlc, -g 是使用lvs工作DR直接路由模式,ipvsadm -h查看帮助)。


 


3、配置realserver脚本


 


[root@test5 chang]#vi realserver


 


#!/bin/sh


 


VIP=192.168.8.11


 


/sbin/ifconfig lo:0 $VIP broadcast $VIP netmask 255.255.255.255 up


 


/sbin/route add -host $VIP dev lo:0


 


echo "1" > /proc/sys/net/ipv4/conf/lo/arp_ignore


 


echo "2" > /proc/sys/net/ipv4/conf/lo/arp_announce


 


echo "1" > /proc/sys/net/ipv4/conf/all/arp_ignore


 


echo "2" > /proc/sys/net/ipv4/conf/all/arp_announce


 


sysctl –p


 


如果有多个realserver直接添加就可以了,之后启动此脚本就可以了.


 


测试:分别启动realserver上的httpd服务


在realserver1 执行 echo "This is realserver1" >> /var/www/html/index.html


在realserver2 执行 echo "This is realserver2" >> /var/www/html/index.html


 


打开IE浏览器输入http://192.168.8.11 应该可以分别看到:This is realserver1 和 This is realserver2.




二、配置基于隧道模式Lvs集群


 


1.配置LVS directorserver 脚本


 


[root@test7 chang]#vi TunLVS


 


#!/bin/sh

VIP=192.168.8.11


 


RIP1=192.168.8.5


 


RIP2=192.168.8.6


 


/etc/rc.d/init.d/functions


 


case "$1" in


 


start)


 


echo "Start Lvs of DirectorServer"


 


#set vip server


 


/sbin/ifconfig tunl0 $VIP broadcast $VIP netmask 255.255.255.255 up


 


/sbin/route add -host $VIP dev tunl0


 


#clear IPVS table


 


/sbin/ipvsadm -C


 


#set lvs


 


/sbin/ipvsadm -A -t $VIP:80 -s rr


 


/sbin/ipvsadm -a -t $VIP:80 -r $RIP1:80 -i


 


/sbin/ipvsadm -a -t $VIP:80 -r $RIP2:80 -i


 


#Run Lvs


 


/sbin/ipvsadm


 


;;


 


stop)


 


echo "Close Lvs DirectorServer "


 


ifconfig tunl0 down


 


/sbin/ipvsadm -C


 


;;


 


*)


 


echo "Usage: $0 {start|stop}"


 


exit 1


 


esac


 


2. 配置realserver


 


[root@test5 chang]#


 


#!/bin/sh


 


VIP=192.168.8.11


 


/etc/rc.d/init.d/functions


 


case "$1" in


 


start)


 


echo "tunl port starting"


 


/sbin/ifconfig tunl0 $VIP broadcast $VIP netmask 255.255.255.255 up


 


/sbin/route add -host $VIP dev tunl0


 


echo "1" > /proc/sys/net/ipv4/ip_forward


 


echo "1" > /proc/sys/net/ipv4/conf/tunl0/arp_ignore


 


echo "2" > /proc/sys/net/ipv4/conf/tunl0/arp_announce


 


echo "1" > /proc/sys/net/ipv4/conf/all/arp_ignore


 


echo "2" > /proc/sys/net/ipv4/conf/all/arp_announce


 


sysctl -p


 


;;


 


stop)


 


echo "tunl port closing"


ifconfig tunl0 down


 


echo "1" > /proc/sys/net/ipv4/ip_forward


 


echo "0" > /proc/sys/net/ipv4/conf/all/arp_announce


 


;;


 


*)


 


echo "Usege: $0 {start|stop}"


 


exit 1


 


esac


 


此脚本分别在realserver上执行,目的使realserver忽略arp响应,并设定vip.


 


三、配置基于高可用Lvs+heartbeat


 


确定LVS使用DR或/tun模式,请对照上面的配置,本例使用DR模式


 


1.配置LVS directorserver 脚本


 


#!/bin/sh


 


VIP=192.168.8.11


 


RIP1=192.168.8.6


 


RIP2=192.168.8.5


 


/etc/rc.d/init.d/functions


 


case "$1" in


 


start)


 


echo "start LVS of DirectorServer"


 


#Set the Virtual IP Address


 


/sbin/ifconfig eth0:1 $VIP broadcast $VIP netmask 255.255.255.255 up


 


/sbin/route add -host $VIP dev eth0:1


 


#Clear IPVS Table


 


/sbin/ipvsadm -C


 


#Set Lvs


 


/sbin/ipvsadm -A -t $VIP:80 -s rr


 


/sbin/ipvsadm -a -t $VIP:80 -r $RIP1:80 -g


 


/sbin/ipvsadm -a -t $VIP:80 -r $RIP2:80 -g


 


#Run Lvs


 


/sbin/ipvsadm


 


;;


 


stop)


 


echo "close LVS Directorserver"


 


/sbin/ipvsadm -C


 


;;


 


*)


 


echo "Usage: $0 {start|stop}"


 


exit 1



esac


 


2. realserver端同样使用上面的配置文件就可以。


 


3.安装heartbeat


 


3.1 安装


 


tar -zxvf libnet.tar.gz


cd libnet


./configure


make


make install


 


groupadd -g 694 haclient


useradd -u 694 -g haclient hacluster


 


tar zxf heartbeat-1.99.4.tar.gz


cd heartbeat-1.99.4


./ConfigureMe configure


make


make install


cp doc/ha.cf doc/haresources doc/authkeys /etc/ha.d/


cp ldirectord/ldirectord.cf /etc/ha.d/


 


3.2配置主文件/etc/ha.d/ha.cf


 


logfile /var/log/ha-log


 


keepalive 2


 


deadtime 60


 


warntime 10


 


initdead 120


 


udpport 694


 


bcast eth0 # Linux


 


auto_failback on


 


ping_group group1 192.168.8.2 192.168.8.3


 


respawn root /usr/lib/heartbeat/ipfail


 


apiauth ipfail gid=root uid=root


 


hopfudge 1


 


use_logd yes


 


node test7


 


node test8


 


crm on


 


3.3资源文件/etc/ha.d/ haresources


 


test7 192.168.8.11 httpd




设置test7为主节点,集群服务器的ip地址为192.168.8.11 集群服务有httpd


 


3.4认证文件(/etc/ha.d/authkeys),选取一种认证方式,这个文件的权限必须是600


 


auth 1


1 crc


#2 sha1 sha1_any_password


#3 md5 md5_any_password


 


chmod 600 /etc/ha.d/ haresources


 


3.5编辑主机名:/etc/hosts


 


192.168.8.2 test8


 


192.168.8.3 test7


 


备份节点192.168.8.3 上的heartbeat和apache的配置与节点1要完全相同,lvs配置也要相同


 


完装完毕进行测试,关闭主节点机器,另一台自动接管,主节点恢复后自动接管回服务。如果以上测试没有问题,那么开始和lvs整合。


 


4.配置Ldirectord


Ldirectord的作用是监测Real Server,当Real Server失效时,把它从Load Balancer列表中删除,恢复时重新添加,在安装heartbeat时已经安装了Ldirectord。


 


配置(/etc/ha.d/ldirectord.cf):


 


checktimeout=3


 


checkinterval=1


 


fallback=127.0.0.1:80


 


autoreload=yes


 


logfile="/var/log/ldirectord.log"


 


quiescent=yes


 


# Sample for an http virtual service


 


virtual=192.168.8.11:80


 


real=192.168.8.6:80 gate


 


real=192.168.8.5:80 gate


 


fallback=127.0.0.1:80 gate


 


service=http


 


request="index.html"


 


receive="Test Page"


 


protocol=tcp


 


checktype=negotiate


 


checkport=80


 


在每个Real Server的中添加监控页:


echo "Test Page" >> /var/www/html/index.html


 


修改/etc/ha.d/haresources


 


test7 192.168.8.11 ipvsadm ldirectord httpd


 


现在可以在主节点启动heartbeat


/etc/init.d/heartbeat start


 


并在备份节点启动heartbeat


 


/etc/init.d/heartbeat start


 


测试:关闭主节点,备份节点将自动接管directorserver服务。


至此配置完毕。