裁云剪水:应用云解决方案

做云这三年

11月3日,是新浪云计算3周年,于是有了这篇文章。但是因为懒,所以现在才写完。完全是个人的一点回顾,不代表官方立场。

从09年回到新浪负责云计算的产品,转眼3年了;新浪云也已经从一个几个人的团队,成长为快50人的部门了。09年的时候,我们还不知道什么叫云计算,现在连专家都能数出来云计算的层次了:PaaS、IaaS和SaaS。

PaaS时期

08年的时候,我创过一次业,主要在校内开放平台(就是现在的人人了)上做社交应用,但我都不好意思和别人讲我是做应用的,因为我一大半的时间都是在弄服务器。

我本来是个PHP程序员,哪儿想过要自己买服务器、自己找IDC托管、自己抱着一台1U的Dell去机房配置网卡地址…… 但只要你想创业,这些都是不可避免的。那时候的我痛恨各种命令行,只会配置GUI界面的软件,前段时间我打开那台服务器倒数据,发现我安装的居然是桌面版的Ubuntu。

一年下来,我发现自己的业务没什么长进,对Linux倒进步神速。虽然每一个PHP程序员上辈子都是折翼的前端、美工和系统管理员,但我依然梦想着有一天,有一个团队能帮我搞定服务器的一切,我只需要把代码放上去,然后只管睡大头觉。

就在这个时候,童剑问我有没有兴趣回去做一个PHP版GAE的产品。本着我不下地狱谁下地狱何况我已经下过地狱的心态,我成为了新浪云的产品经理。…

通过PHP的Wrapper无缝迁移原有项目到新服务

出于性能和安全方面的考虑,公司的平台上禁用了本地文件读写和对外的数据抓取.相应的,我们提供了对应的服务来做同样的事情.新服务的接口和原来不太一样.

专门为我们平台开发的程序当然不会存在问题,但是有大量的已有的程序和开源项目,就面临着繁杂的迁移工作.

Wrapper

其实从PHP4.3开始,PHP就支持Wrapper了,这意味着用户可以自定义和重载协议.

只需要使用 stream_wrapper_register 函数就可以注册一个协议,对这个协议的相关操作,PHP都会回调相关的函数.

手册上给了一个例子. 它注册了一个叫var的协议,然后对这个协议操作都会回调VariableStream class里边定义的方法.


varname = $url["host"];
$this->position = 0;

return true;
}

function stream_read($count)
{
$ret = substr($GLOBALS[$this->varname], $this->position, $count);
$this->position += strlen($ret);
return $ret;
}

function stream_write($data)
{
$left = substr($GLOBALS[$this->varname], 0, $this->position);
$right = substr($GLOBALS[$this->varname], $this->position + strlen($data));
$GLOBALS[$this->varname] = $left . $data . $right;
$this->position += strlen($data);
return strlen($data);
}

function stream_tell()
{
return $this->position;
}

function stream_eof()
{
return $this->position >= strlen($GLOBALS[$this->varname]);
}

function stream_seek($offset, $whence)
{
switch ($whence) {
case SEEK_SET:
if ($offset < strlen($GLOBALS[$this->varname]) && $offset >= 0) {
$this->position = $offset;
return true;
} else {
return false;
}
break;

case SEEK_CUR:
if ($offset >= 0) {
$this->position += $offset;
return true;
} else {
return false;
}
break;

case SEEK_END:
if (strlen($GLOBALS[$this->varname]) + $offset >= 0) {
$this->position = strlen($GLOBALS[$this->varname]) + $offset;
return true;
} else {
return false;
}
break;

default:
return false;
}
}
}

stream_wrapper_register("var", "VariableStream")
or die("Failed to register protocol");

$myvar = "";

$fp = fopen("var://myvar", "r+");

fwrite($fp, "line1n");
fwrite($fp, "line2n");
fwrite($fp, "line3n");

rewind($fp);
while (!feof($fp)) {
echo fgets($fp);
}
fclose($fp);
var_dump($myvar);

?>

回调class里边能实现的接口列表在这里: http://cn2.php.net/manual/en/class.streamwrapper.php

需要注意的一些问题

构造函数

首先是,wrapper class很特别,它的构造函数并不是每次都调用的.只有在你的操作触发了stream_open相关的操作时才会调用,比如你用file_get_contents了.而当你的操作触发和stream无关的函数时,比如file_exists会触发url_stat方法,这个时候构造函数是不会被调用的.

读实现

wrapper里边有position和seek等概念,但是很多服务其实是一次性就读取全部数据的,这个可以在stream_open的时候一次性读回,放到一个属性中,以后seek和tell的时候直接操作属性里边存放的数据就可以了.

url_stat的实现

在wrapper class的实现中,url_stat的实现是个难点.必须正确的实现url_stat才能使is_writable和is_readable等查询文件元信息的函数正常工作.

而我们需要为我们的虚设备伪造这些值.以mc为例,我给大家一些参考数据.

url_stat应该返回一个数组,分13个项,内容如下:

dev 设备号- 写0即可
ino inode号 – 写0即可
mode 文件mode – 这个是文件的权限控制符号,稍后详细说明
nlink link – 写0即可.
uid uid – Linux上用posix_get_uid可以取到,windows上为0
gid gid – Linux上用posix_get_gid可以取到,windows上为0
rdev 设备类型 – 当为inode设备时有值
size 文件大小
atime 最后读时间 格式为unix时间戳
mtime 最后写时间
ctime 创建时间
blksize blocksize of filesystem IO 写零即可
blocks number of 512-byte blocks allocated 写零即可

其中mode的值必须写对
如果是文件,其值为
0100000 + 文件权限 ; 如 0100000 + 0777;
如果是目录,其值为
040000 + 目录权限 ; 如 0400000 + 0777;

可以重载标准协议

根据实际测试来看,用stream_wrapper_unregister可以卸载掉http等内置协议.这就方便我们完全无缝的替换用户的一些操作,比如file_get_contents(‘http://sae.sina.com.cn’)到我们自己实现的服务上.…

使用percona的mysql补丁统计Mysql使用情况

在应用粒度进行mysql服务的管理相对简单,只要把mysql账号和应用账号绑定起来就可以了.

mysql账号的管理可以通过向mysql库user表增删记录来实现.

要想给某个用户只分配特定库的权限时,可以将user表中权限字段全部设置为N,然后向mysql库下的db表添加记录指定特定库权限.

接来下我们讨论如何获取详细的mysql使用信息,例如某个账号的数据库大小,写入字节数和读取字节数,累计消耗的cpu时间.

mysql本身没有提供这些数据.但是percona在google发布的mysql patch上进行了修改,完成了一个userstatus的功能.

其原理非常简单,就是在information_schema库中添加了一些内存表,里边存放了对应的数据.

安装percona-mysql

我们选择下载percona打好补丁的源代码编译后安装的方式.你也可以下载编译好的二进制版本.


wget http://www.percona.com/mysql/5.0.87-b20/source/mysql-5.0.87-percona-src.tar.gz
tar xzvf mysql-5.0.87-percona-src.tar.gz
cd mysql-5.0.87

在配置之前,先安装一些会用到的包


sudo apt-get install libncurses5-dev byacc

开始编译


./configure --prefix=/home/easy/dev/mysql5/
make
make install

使用percona-mysql

安装完成后,mysql下数据目录没有,建立mysql数据库

mkdir data
chown mysql:mysql data
bin/mysql_install_db --user=mysql
--basedir=/home/easy/dev/mysql5
--datadir=/home/easy/dev/mysql5/data

mysql就配置完成了.启动mysql


bin/mysqld_safe &

使用phpmyadmin连接上后,发现information_schema中会多出以下表:

分别从 客户端,索引,用户和表的角度为我们提供了统计数据.各个表的字段和相关说明可以参见wiki.

浏览各个表发现都还没有数据.这是因为默认数据统计都没有开放的.

在phpmyadmin中执行以下SQL


SET GLOBAL userstat_running = 1

然后我们就能在数据表中查看到信息了.

为避免每次启动服务时都手工执行此命令,我们可以把 userstat_running = 1 添加到 my.cnf 中.

数据表的大小可以通过TABLES表中的data_length和index_length字段相加获得.

基于percona-mysql的配额控制

有了这些数据,进行Mysql服务的配额限制就是非常简单的事情了.

写一个php脚本,定时检查各个user的数据用量,然后和分配给该用户的配额上线比较,一旦超过,通过修改mysql库中的user和db表收回该用户的mysql权限即可.…

使用Pure-ftpd和Pure-ftpd-mysql进行FTP权限和磁盘配额管理

在上一篇文章里边,我们已经完成了利用mod-myvhost动态的添加web用户.这里我们接着来完成文件上传的管理.

我们选用Pure-ftpd作为工具.

安装

Pure-ftpd是一个成熟的ftp工具,apt-get已经有打好的包了.我们直接通过apt-get就能很方便的安装.

apt-get install pure-ftpd-mysql

配置

在数据库里边建一个库,这里用之前的hosting数据库.用以下语句创建一张表:

CREATE TABLE users(
User varchar(16) NOT NULL default '',
status enum('0','1') NOT NULL default '0',
Password varchar(64) NOT NULL default '',
Uid varchar(11) NOT NULL default '-1',
Gid varchar(11) NOT NULL default '-1',
Dir varchar(128) NOT NULL default '',
ULBandwidth smallint(5) NOT NULL default '0',
DLBandwidth smallint(5) NOT NULL default '0',
comment tinytext NOT NULL,
ipaccess varchar(15) NOT NULL default '*',
QuotaSize smallint(5) NOT NULL default '0',
QuotaFiles int(11) NOT NULL default 0,
PRIMARY KEY (User),
UNIQUE KEY User (User)
) TYPE=MyISAM;

这张表将用来存放ftp用户的相关信息.

然后我们需要修改pure-ftpd-mysql的配置文件,告诉pure-ftpd数据库的相关信息.
这里假设大家已经配置好了mysql,并有一个可用的账号.

vim /etc/pure-ftpd/db/mysql.conf

放入以下配置

#账号信息 按自己情况修改
MYSQLServer 127.0.0.1
MYSQLPort 3306
MYSQLSocket /var/run/mysqld/mysqld.sock
MYSQLUser easy
MYSQLPassword ******
MYSQLDatabase hosting

#加密方式
MYSQLCrypt md5

#取得数据的SQL语句

#用户授权
MYSQLGetPW SELECT Password FROM users WHERE User="L"
MYSQLGetUID SELECT Uid FROM users WHERE User="L"
MYSQLGetGID SELECT Gid FROM users WHERE User="L"
MYSQLGetDir SELECT Dir FROM users WHERE User="L"

#文件大小和个数限制
MySQLGetQTAFS SELECT QuotaFiles FROM users WHERE User="L"
MySQLGetQTASZ SELECT QuotaSize FROM users WHERE User="L"

#上行和下行带宽限制
MySQLGetBandwidthUL SELECT ULBandwidth FROM users WHERE User="L"
MySQLGetBandwidthDL SELECT DLBandwidth FROM users WHERE User="L"

这样pure-ftpd就知道如何从mysql里边取数据了.
为了得到上边出现的GID和UID,我们需要为ftp创建一个虚拟账号.FTP的全部用户共用这一个虚拟账号的UID和GID,而不需要为每个FTP用户创建系统用户.


groupadd -g 2001 ftpgroup
useradd -u 2001 -s /bin/false -d /bin/null -c "pureftpd user" -g ftpgroup ftpuser

我们创建了GID为2001的ftpgroup组和UID为2001的ftpuser用户.现在可以往数据表中添加数据了.QuotaSize的单位是M,目录指向我们在mod-myvhost同样的路径.

然后我们将用户锁定在他自己的目录下,不允许他通过ftp访问到别人的目录:


echo yes > /etc/pure-ftpd/conf/ChrootEveryone

当指定目录不存在时,自动创建目录

echo yes > /etc/pure-ftpd/conf/CreateHomeDir

设置完成.重启pure-ftpd.

/etc/init.d/pure-ftpd-mysql restart

全部配置都完成了.我们上传文件试试.
登陆成功,上传文件提示没权限.这是因为/data0/myapphost的属性设定造成的.将整个目录改为ftpuser的就可以了


chown -R ftpuser:ftpgroup /data0/myapphost

修改属性后,已经可以上传文件了.试试配额限制:


错误:> [2010-1-27 22:47:12] 无法写入数据 socket。Socket 错误 = #10054。
[2010-1-27 22:47:12] 550-Quota exceeded: abc.zip won't be saved
550-6 files used (120%) - authorized: 5 files
550 6974 Kbytes used (681%) - authorized: 1024 Kb
错误:> [2010-1-27 22:47:12] 请求的操作未执行(如,文件或目录未找到,不能访问)。

错误:> [2010-1-27 22:47:43] 无法写入数据 socket。Socket 错误 = #10054。
[2010-1-27 22:47:43] 550-Quota exceeded: abc.zip won't be saved
550-4 files used (80%) - authorized: 5 files
550 3954 Kbytes used (386%) - authorized: 1024 Kb
错误:> [2010-1-27 22:47:43] 请求的操作未执行(如,文件或目录未找到,不能访问)。

从错误信息可以看出,配额已经生效了.当配置修改后,已经登录的ftp用户需要重新登陆后才会生效.

小结

到这里为止,我们已经完成了一个单服务器的应用管理平台的基本功能.再添加一个用于申请应用和开通账号的web管理前端,整个平台即可投入使用了.

下次我们将讨论,对于作为这个平台上的服务的Mysql,如何进行配额的限制.…

在Apache2.2.XX下安装Mod-myvhost模块

上回提到Mod-myvhost只有for apache1.3的版本,后来google了下,发现一个葡萄牙的同学讲到了如何在2.x上安装(围观请翻墙,并自备翻译工具),才发现mod-myvhost的svn分支里边有2.0版本的代码(部分运气不好的围观群众请继续翻墙).于是费了一番功夫,我终于把这个模块装上了.下边是安装过程.

首先,因为代码里边用到了mysql的一些头文件,所以要安装mysql的开发包.


sudo apt-get install libmysql++-dev

然后,这个apache module采用apxs编译,所以apxs也要有.如果你已经编译过apache了,那么apxs就在bin下边,和apachectl在同一级目录下.

准备工作就是这些.开始安装mod-myvhost.

svn checkout代码.

svn co http://mod-myvhost.googlecode.com/svn/branches/2.0.xx/

为什么要用2.0.xx而不用2.2.xx-dbd? 因为2.2.xx-dbd编译通不过…有兴趣的同学可以自行debug去.

然后进入目录,开始编译


cd 2.0.xx/
make

呜,报错了


/bin/sh: apxs: command not found
make: *** [mod_myvhost.so] Error 127

原来是apxs没有找到.修改Makefile,指定apxs路径,vim Makefile,第四行改为


APXS = /home/easy/dev/apache2/bin/apxs(这里换成你apache2/bin目录的路径/apxs)

然后 make,make install . 一切ok. 查看apache2/conf/httpd.conf,已经自动加上了相关的配置.


LoadModule myvhost_module modules/mod_myvhost.so

试着启动下.又挂了… 提示信息如下:

undefined symbol: apr_hash_clear

grep 源代码发现只有mod_myvhost_cache.c的最后一段用到.


/* FIXME: older */
void cache_vhost_flush(myvhost_cfg_t *cfg, apr_hash_t *cache, time_t older)
{
if (!cfg->cache_enabled) {
return;
}
if (!cache) {
return;
}
apr_hash_clear(cache);
}

#endif /* WITH_CACHE */

修改Makefile,先关掉cache部分


#CFLAGS+= -DWITH_PHP -DWITH_CACHE
CFLAGS+= -DWITH_PHP

重新编译安装后启动apache.Ok了.接着测试动态host功能.
修改apache配置文件 httpd.conf,添加myvhost段.

<IfModule mod_myvhost.c>
MyVhostOn           on
MyVhostDefaultHost  "appgame.cn"
MyVhostDefaultRoot  "/data0/myapphost/"
MyVhostDbHost       "127.0.0.1"
MyVhostDbSocket     "/tmp/mysql.sock"
MyVhostDbUser       "easy"
MyVhostDbPass       "***"
MyVhostDbName       "hosting"
MyVhostQuery        "SELECT rootdir,admin,extra_php_config FROM vhosts WHERE vhost='%s' AND enabled='yes'"
<Directory "/data0/myapphost/">
Options Indexes
AllowOverride None
Order allow,deny
Allow from all
</Directory>
</IfModule>

在phpmyadmin里边导入2.0.xx/vhost.sql,会自动建立hosting库.删掉里边的示例数据,填入我们需要的数据:

在对应的目录下建立测试文件index.php.重启apache已经可以正常工作了.

mod-myvhost会自动添加open_basedir限制,写段代码测试下:

echo file_get_contents( '../02/o.php' );

访问页面,输入如下:

Warning: file_get_contents() [function.file-get-contents]: open_basedir restriction in effect. File(../02/o.php) is not within the allowed path(s): (/data0/myapphost/01/) in /data0/myapphost/01/index.php on line 1

Warning: file_get_contents(../02/o.php) [function.file-get-contents]: failed to open stream: Operation not permitted in /data0/myapphost/01/index.php on line 1

看起来基本的vhost功能已经完成了.

接下来要解决的几个问题是

  1. cache是一个重要的功能,想办法把cache的问题查出来.apr的源代码里是有这个函数的,可能是因为apr的路径或者版本不对.
  2. 代码访问的隔离完成了.但是代码上传的问题还没有解决,接着开始ftp server的选型.要求能通过文件或者mysql的方式方便的添加账号,能根据添加的账号指定目录的磁盘配额.

这些是下个周末的功课 :] .…