存档

‘运维’ 分类的存档

Puppet笔记:介绍及基本用法

2009年6月5日

Puppet是一个可以管理大量linux主机的系统,它的中心思想是让主机上的某个资源与配置文件(manifest)对该资源的设计一致,这里资源可以指一个文件、一个用户、一个软件包、一条cron中的任务等实体。例如下面的一段manifest:

 

file { “/etc/sudoers”:
    owner => root,
    group => root,
    mode => 440
}

它的作用就是指明了:/etc/sudoers这个文件,其owner应为root,group应为root,mod应为440。执行的时候,客户端会判断这三点是否符合,如果不符合,它会去设置user、group、mode等的值为设置的值。

安装

Puppet的结构也是一个唯一(以后会有failover冗余)的中心Server与所有的Client组成。

Puppet是基于Ruby写成的,所以安装前要准备好Ruby环境,并且需要安装RubyGems以用来安装。Server与Client的安装没有区别,都在相同的包内,只是中心的server上运行的是puppetmasterd,被管理机上运行的是puppetd。另外,每台主机上一定要配置好自己的hostname,之后每台机器要以hostname区分。

如果ruby环境和gems都装好了,执行下面几句就可以安装好puppet了:

wget puppet.proximic.cnpuppet/facter-1.5.5.gem
gem install facter-1.5.5.gem
wget puppet.proximic.cn/puppet/puppet-0.24.8.gem
gem install puppet-0.24.8.gem

使被管理机与中心机连接

server端首次运行前,编辑/etc/puppet/manifests/site.pp文件,内容可以用最基本的:

 

# site.pp
file { "/etc/sudoers":
    owner => root, group => root, mode => 440
}

 

启动server,执行:puppetmasterd –mkusers,这个参数是为了生成puppet运行所用用户,以后执行就不需要了。

然后在客户机上执行:puppetd –server <你的服务器地址> –waitforcert 60 –test,这时客户机会去连server,但是由于连接是在ssl上的,而Server还没有sign过客户的cert,这时客户机被被断开。到server上执行:puppetca –list,会显示出刚才这个客户的hostname为等待签名的客户,执行:puppetca –sign <客户hostname> 即可为其签名。这时再到客户机上执行上面的puppetd命令,即可看到客户在正常地连接server,并且检测上面关于sudoers文件的策略了。

向被管理机分发文件

通过puppet可以向被管理机上推送文件,方法是使用file类型的source属性,例如:

file {
    ”/etc/profile.d/java.sh”:
    source => “puppet://puppet.domain.com/profiles/javaprofile.sh”,
    owner => root,
    group => root,
    mode => 755
}

这里的文件来源是什么呢?puppetmasterd自带一个简易的文件服务器,配置方法是创建并编辑/etc/puppet/fileserver.conf ,其中每个section是一个module,module就是指上述puppet://路径中域名之后的第一段(即“profiles”),下面可以配置该module对应的server上的物理路径以及acl等,例如:

[profiles]
    path /home/misc/profiles
    allow *

这样如果客户端上该文件不存在,或者内容不同(通过checksum),就会从server上把这个文件拉过去。

执行脚本

很复杂的部署任务肯定不能完全靠puppet完成,这时我会写一个部署用的脚本。Puppet有一种“资源”叫exec,通过它可以执行程序,还可以设置执行时的path,环境变量,user/group,是否记录output等,很方便,例如:

exec { “/root/puppet/nagiosclient/nrpe.sh”:
    cwd => “/root/puppet”,
    timeout => 7200,
    logoutput => on_failure,
    user => root,
    path => ["/sbin", "/usr/sbin", "/usr/local/sbin", "/root/bin", "/usr/local/bin", "/usr/bin", "/bin", "/usr/lib64/jvm/jre/bin"],
    require => File["/root/puppet/nagiosclient/nrpe.sh"]
}

想在各客户机上执行时,我一般就是先用上述file资源的配置把该脚本推送到客户机上,然后用空上exec资源去执行它,注意exec中要配置好require 该file,否则无法保证exec在file之类在客户上执行。

运维 ,

原来mount的时候还能用硬盘id来mount

2009年3月5日

不知道是不是我土了。。。

原来mount硬盘的时候都写成/dev/sda,/dev/hda1之类的型式,solaris下面也不过是硬盘的编号。这样的缺点就是如果你添减硬盘(最典型的例子是装系统时接着光驱,装完后拨了它编号就变了)编号变了会出问题

今天才发现mount的dev的名字原来可以写成这样:

/dev/disk/by-id/ata-ST3250820NS_9QE68B2F
/dev/disk/by-id/scsi-SAdaptec_RAIDNAME_2244073F-part1

然后fstab里也用这种格式写,一下子就少了很多麻烦了嘛

运维

记一次网站紧急维护过程

2008年2月25日

学院自己设计的选课系统,今年初次启用,用于一个年级约150人的同时选课。选课开始之后很快网站便失去响应。重启服务器之后重新开启选课系统,但是又迅速停止服务。于是选课宣布推迟。

 


二次选课安排在某日下午。于第二次选课前一天接手站点维护,对站点的结构和程序性能进行分析。该站点由两台服务器分别负责数据库和Web服务。采用10个
并发用户模拟选课过程时,210个事务操作需要10分钟左右。期间Web服务器基本没有负载,而数据库服务器CPU满载。站点数据表尺寸在1M的级别,程
序代码中仅有一次锁表操作,但是有若干处在循环中查询数据库。因此推测性能问题是由于对数据库访问次数过多而导致的。

 

开启
数据库的日志并访问课程列表页面,发现仅此一个页面就有200余次数据库查询操作。对课程列表页面进行压力测试发现,数据库服务器每秒可处理近800次查
询请求。而一次完整的选课过程则会导致900余次数据库查询。这说明程序结构存在着严重的问题,因此对程序进行重构,消除循环中对于数据库的访问。共将三
处循环中对于数据库的访问改为存储过程,另外修改了一些查询语句。同时对数据表项目进行了小的调整,添加了几个表项作为缓存,消除了几处多余查询和重复查
询。撤销了两个被频繁调用的且结果稳定的函数中对于数据库的调用,将部分结果暂时硬编码至程序中。另有一处被频繁调用的函数因程序结构原因无法进行快速重
构,故而放弃对其的修改。经过上述重构,显示课程列表页面所需的访问数据库次数降低至20次左右,同时由于数据库和程序客户端的问题增加了三次额外的数据
库连接操作。应当说这个数据库访问次数仍是偏多的,但是与原先的情况相比有了明显的改进,同时由于程序结构的限制,在短时间内无法进行进一步的优化,因此
对程序的优化到此为止告一段落。整个重构过程耗时约12小时。在程序重构的同时,根据网站页面的功能对于部分网页暂时采取了转为静态页面和不显示的方法以
降低重构的工作量。优化结束后对课程列表页面进行压力测试,10个客户端连续刷新时Web服务器的CPU占用在40%左右,而数据库服务器的CPU占用在
30%附近。

 

程序重构结束之后,对数据库以及两台服务器的操作系统继续进行优化。根据程序中的查询语句为数据库增添了一些
索引,从而使得10个客户端连续刷新课程列表页面时数据库服务器的CPU占用降低至10%以下。此时Web服务器的CPU占用相对较高,同时使用压力测试
软件进行测试时响应时间有明显的周期性分布。检查Web服务器的网络连接情况,发现Web服务器端访问数据库的网络连接在程序结束之后没有及时关闭,猜测
这可能导致网络连接数过多,从而导致响应时间周期性的分布。因此调整Web服务器的部分网络参数以消除此现象。另外为Web服务器安装了程序脚本的预编译
模块以避免重复的脚本编译操作。经过这些优化之后,25个客户端连续刷新课程列表页面时Web服务器的CPU占用降低至20%以下,数据库服务器的CPU
占用在15%左右。

 

分析维护前后系统的性能,对于程序的重构使响应速度提高了近10倍;添加新的数据库索引以及Web程序脚本预编译又各自使得Web服务器和数据库服务器的响应速度翻倍。因此总的来看,维护之后站点的性能提高了近20倍。

 


日上午对于网站的内容以及程序逻辑进行进一步的检查,更正了几处数据和逻辑错误。下午选课时,网站负载在选课开始3分钟后达到峰值。Web服务器的峰值
CPU占用在15%,而数据库服务器的峰值CPU占用在10%。选课开始10分钟后两服务器已没有明显负载。根据数据库内容的监测,此时大部分学生的选课
已基本结束。

运维 , ,

一些安装SVN的笔记

2007年10月5日

这里的内容是安装与apache结合的svn服务器端的记录

apache2:
./configure –enable-dav –enable-so –enable-maintainer-mode

subversion:
./configure(如果apache2不是安在默认目录了,会报错,要加参数指明apr和apxs的位置)
make
make install

在httpd.conf中:
make install应该已经添加了的内容:
LoadModule dav_svn_module     modules/mod_dav_svn.so
LoadModule authz_svn_module   modules/mod_authz_svn.so
加入:
<Location /svn> (/svn是用户浏览时url中的路径)
        DAV svn
        SVNListParentPath on
        SVNParentPath /home/svn    (这个目录是实际数据存储的目录)
        AuthType Basic
        AuthName “Subversion repositories”
        AuthUserFile passwd
        #AuthzSVNAccessFile svnaccessfile
        Require valid-user
</Location>
在/usr/local/apache2下,bin/htpasswd -c passwd loudly

创建分支时
cd /home/svn svnadmin create repname

http://server-IP/svn/repname

运维 ,

Linu下不输入密码连接远程主机的方法

2007年4月22日

zz from http://blog.csdn.net/colin719/archive/2007/01/16/1484469.aspx

SSH不输入密码连接远程Linux主机

机 理:公/私密钥验证。公钥(public key)用于加密,私钥(private key)用于对使用其匹配的公钥加密的数据进行解密。在本地机器生成一个密钥对,把公钥放到远程主机,然后从本地机器发起ssh连接,远程主机的sshd 产生一个随机数并用此公钥进行加密后发给本地机器,本地机器使用私钥进行解密并将结果发回,远程主机验证结果无误后准予登陆。

步骤:
注:密钥需要与远程Linux主机上的SSH系统相匹配。openSSH是Linux上默认的SSH系统,因此这里的内容只适用于openSSH。
1. 生成密钥对。

$ ssh-keygen -t rsa
这 个命令生成一个密钥对:id_rsa和id_rsa.pub。他们默认被保存在~/.ssh/目录下。可以将id_rsa.pub改为一个易于辨识这是谁的密钥的名字。在生成过程中会要求输入pass phrase,这个是用来保护私钥的使用的,即每次你使用私钥的时候需要先输入这个密码,因此这里不要输入任何字符,直接回车。

2. 上传公钥。
将生成的公钥,如test.pub,上传到远程主机的~/.ssh目录下,将test.pub的内容附加到authorized_keys文件末尾。

3. 远程主机SSH的设置
authorized_keys必须只有所有者才能访问:
$ chmod 600 ~/.ssh/authorized_keys
(下面两条语句我并没有用到)
另外,为了不在每次发起连接时输入pass phrase:
$ ssh-agent $SHELL
$ ssh-add
(注:这个是参照MPICH的SSH设置,我还不确定如果不使用MPICH的话这个是不是必需的。)

4. 连接

在Linux终端下,直接输入ssh remote_machine_name然后即可发起ssh连接,远程系统将进行公钥认证。

运维

Proftpd的权限设置(zz)

2007年3月31日
 
proftpd默认用户可以使用系统非root组的用户登录,登陆后都在自己的/home目录中。
同时匿名用户不能登陆。而要对权限进行进一步的设置,需要在proftpd.conf里面进行定制。
在默认的conf中,有如下的例子
# <Anonymous ~ftp>
#   User    ftp
#   Group    nogroup
#   # We want clients to be able to login with “anonymous” as well as “ftp”
#   UserAlias   anonymous ftp
#   # Cosmetic changes, all files belongs to ftp user
#   DirFakeUser on ftp
#   DirFakeGroup on ftp
#
#   RequireValidShell  off
#
#   # Limit the maximum number of anonymous logins
#   MaxClients   10
#
#   # We want ‘welcome.msg’ displayed at login, and ‘.message’ displayed
#   # in each newly chdired directory.
#   DisplayLogin   welcome.msg
#   DisplayFirstChdir  .message
#
#   # Limit WRITE everywhere in the anonymous chroot
#   <Directory *>
#     <Limit WRITE>
#       DenyAll
#     </Limit>
#   </Directory>
#
#   # Uncomment this if you’re brave.
#   # <Directory incoming>
#   #   # Umask 022 is a good standard umask to prevent new files and dirs
#   #   # (second parm) from being group and world writable.
#   #   Umask    022  022
#   #            <Limit READ WRITE>
#   #            DenyAll
#   #            </Limit>
#   #            <Limit STOR>
#   #            AllowAll
#   #            </Limit>
#   # </Directory>
#
# </Anonymous>
proftpd的配置文件的格式和apache很相似:
 
#全局设置
设置项目1 参数1
设置项目2 参数2
#某个目录的设置
<Directory “路径名”>

</Directory>
#关于匿名用户的设置
<Anonymous “匿名登陆的目录”>

<Limit 限制动作>
..
</Limit>
</Anonymous>
其中最重要的就是limit之中的部分,涉及到了具体的权限控制

CMD:Change Working Directory 改变目录
MKD:MaKe Directory 建立目录的权限
RNFR: ReName FRom 更改目录名的权限
DELE:DELEte 删除文件的权限
RMD:ReMove Directory 删除目录的权限
RETR:RETRieve 从服务端下载到客户端的权限
STOR:STORe 从客户端上传到服务端的权限
READ:可读的权限,不包括列目录的权限,相当于RETR,STAT等
WRITE:写文件或者目录的权限,包括MKD和RMD
DIRS:是否允许列目录,相当于LIST,NLST等权限,还是比较实用的
ALL:所有权限
LOGIN:是否允许登陆的权限

针对这些设置,又有如下具体的配置:

AllowUser 针对某个用户允许的Limit
DenyUser 针对某个用户禁止的Limit
AllowGroup 针对某个用户组允许的Limit
DenyGroup 针对某个用户组禁止的Limit
AllowAll 针对所有用户组允许的Limit
DenyAll 针对所有用户禁止的Limit

同时,可以针对单独的用户来限制速度

TransferRate STOR|RETR 速度(Kbytes/s) user 使用者

而对于虚拟用户,无法登陆的。所以,必须修改<Anonymous ~ftp>为<Anonymous 你要指定的主目录>

下面是我的配置
<Anonymous /ftp/ftphome>
User    ftp                                                        #指定用户的组和名称
Group    nogroup
UserAlias   anonymous ftp                        #使得ftp和匿名用户都能登陆
DirFakeUser on ftp
DirFakeGroup on ftp
RequireValidShell  off                                
MaxClients   50                                           该用户的最大连接数
DisplayLogin   welcome.msg                  #显示欢迎信息,需要注意把msg文件放到登陆后的主目录
DisplayFirstChdir  .message
MaxClientsPerHost   3                             #限制每个主机最大连接数
<Directory *>                                              #这里是对目录进行设置,即不允许写
 <Limit WRITE>
  DenyAll
 </Limit>
</Directory>
<Directory incoming>                                #对上传目录的设置,我们有一个incoming文件夹需要允许别人上传
Umask    022  022
 <Limit READ>                                           #不允许下载
  DenyAll
 </Limit>
 <Limit STOR MKD>                                    #允许上传和新建目录
  AllowAll
 </Limit>
</Directory>
</Anonymous>
 
同时,我们还需要对ftp进行管理。所以在系统中建立一个用户,名称为ftpadmin,属于nogroup组,不允许登陆。同时赋予它对ftp所有的权限
<Anonymous /ftp/ftphome>
User    ftpadmin
Group    nogroup
 <Directory *>
  <Limit ALL>
   AllowAll
  </Limit>
 </Directory>
</Anonymous>
 

运维

mysql客户程序的中文乱码问题

2007年3月30日

自mysql5.0以来,自己的客户端程序在编码问题上越来越复杂了,乱码是常见的。

一般来说,对于想使用中文的情况(包括用C写的程序或是php等),在连接上数据库之后,应该执行一下set names ‘gb2312′(或是别的编码),这时再使用中文就是正常的了。

不过今天遇到的问题源于proftpd,用proftpd与mysql相连实现用户认证,而用户的家目录也是存在数据库中的,而我们FTP的家目录中含有中文,这样mysql返给proftpd就是乱码了,无法登录。可是你有没办法让proftpd给mysql发一个set names的命令,所以问题应该这么解决。编辑/etc/my.cnf,在其中加一个[client]字段,其下加一句:default-character-set=gb2312,这样应该就没问题了,相当于本机所有的mysql客户程序都默认用gb2312去连别的数据库。你可以用mysql程序连数据库,用show variables;命令来看看编码的几项是不是已经成了gb2312。不过这样还是不甚好,因为一个机器上的客户程序还是可能连多个数据库的,如果几个数据库用的编码不同,还是不成,所以如果条件许可,还是应该在程序执行时调用set names来指定当前连接的编码。

运维 ,

Memcached深度分析(转载)

2007年3月18日

//奶瓶同学滴文章,哼哼
//来源:http://www.54np.com/docs/mc.html

 

Memcached深度分析

作者:奶瓶
网站:http://www.54np.com

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>intdaemon(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中,换行符号统一为rn

◎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 behavīor */    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 %5dn",                    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:值长(包括换行符号rn)
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            ōper = 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的相关开发。

◎后记

这是我在农历丙戌年(我的本命年)的最后一篇文章,由于Memcached的内涵很多,仓促整理一定有很多遗漏和错误。感谢新浪网提供的研究机会,感谢部门同事的帮助。

运维, 技术 ,

一些关于远程局域网接入、proxyarp的笔记

2007年3月3日

今天实现了把通过VPN拨入的远程主机加到本地的内网。

用了一些时间,其实核心步骤就是这些:

1.首先在本地局域网可与外网相连的边界服务器上建立vpn服务器。要设置服务器端IP与该机接内网的物理网卡所在的地址在同一个网段。还要设置允许客户机自己指定其地址。(具体设置方法详见http://space.loudly.cn/index.php/2/action_viewspace_itemid_77.html

2.开启proxyarp,其作用就是当物理局域网上的其它机器相与远程拨入者通信时,发arp请求,这个vpn服务器会把自己网卡的MAC返回给它,方法如下:

echo 1 > /proc/sys/net/ipv4/conf/eth1/proxy_arp

其中eth1就是指该边界服务器接局域网的网卡

3.要求远程主机设置静态路由,例如route add -net 192.168.1.0/24 ppp0,由于vpn是点到点连接的,虽然地址与局域网是同一个网段,但是还是要加这个路由的。

由上设置后,远程主机与物理局域网上的主机就能互联了,而且不同的远程主机之间也可以互联了。
我又用win2k3在路由与远程访问服务中添加了一个虚拟端口拨到这个linux的服务器上,并用nat带着另一个内网的机器也连到了该局域网,好强大啊。

网络, 运维 , ,

linux启动时只显示GRUB的问题

2007年1月20日

启动打linux rescue进入rescue模式
然后chroot到/mnt/sysimage/
fdisk -l看一下硬盘顺序对不对
然后cat /boot/grub/device.map,看看启动的硬盘对应的括号里的号对不对,也就是hd0,hd1等等
如果和你希望的不同,改之
然后执行grub-install /dev/hdX,hdX就是指要安grub的那个硬盘
再编辑/etc/grub.conf,重启就行了

运维