对于绝大多数发展中等的web 2.0网站来说,LAMP结构已经不能满足现在的需要了,新的架构组合是GLAMMP,G=Gearman(分布式远程过程处理),M=Memcached(高性能的分布式的内存对象缓存系统)。
Gearman的高级特性
在一个 Web 应用程序内可能有许多地方都会用到 Gearman。可以导入大量数据、发送许多电子邮件、编码视频文件、挖据数据并构建一个中央日志设施 — 所有这些均不会影响站点的体验和响应性。可以并行地处理数据。而且,由于 Gearman 协议是独立于语言和平台的,所以您可以在解决方案中混合编程语言。比如,可以用 PHP 编写一个 producer,用 C、Ruby 或其他任何支持 Gearman 库的语言编写 worker。
一个连接客户机和 worker 的 Gearman 网络实际上可以使用任何您能想象得到的结构。很多配置能够运行多个代理并将 worker 分配到许多机器上。负载均衡是隐式的:每个可操作的可用 worker(可能是每个 worker 主机具有多个 worker)从队列中拉出作业。一个作业能够同步或异步运行并具有优先级。
Gearman 的最新版本已经将系统特性扩展到了包含持久的作业队列和用一个新协议来通过 HTTP 提交工作请求。对于前者,Gearman 工作队列保存在内存并在一个关系型数据库内存有备份。这样一来,如果 Gearman 守护程序故障,它就可以在重启后重新创建这个工作队列。另一个最新的改良通过一个 memcached 集群增加队列持久性。memcached 存储也依赖于内存,但被分散于几个机器以避免单点故障。
Gearman 是一个刚刚起步却很有实力的工作分发系统。据 Gearman 的作者 Eric Day 介绍,Yahoo! 在 60 或更多的服务器上使用 Gearman 每天处理 600 万个作业。新闻聚合器 Digg 也已构建了一个相同规模的 Gearman 网络,每天可处理 400,000 个作业。Gearman 的一个出色例子可以在 Narada 这个开源搜索引擎(参见 参考资料)中找到。
大规模web架构gearman分布式处理应用案例
业务服务压力比较大,想把一些占用资源的功能异步到远程处理,比如记录业务日志,文件加密,文件分发到其他文件服务器节点上,检查文件服务器是否已同步,对用户上传的图片进行剪裁生成多份缩略图,视频转换,静态内容生成,清除缓存等等,这些请求耗时长,占用系统资源大,影响业务正常访问。这些问题会经常遇到的,如果这些任务都在用户请求过程中完成,服务器撑不撑得住暂不考虑,单凭用户体验角度考虑来说,那是难以忍受的。
对于这种需求,我们可以通过分布式计算,对任务进行拆分,转移到多台服务器上进行异步或同步处理。分布式消息队列有多种实现方式如rabbitmq、gearman等。
在使用异步处理之前,当我们要发送邮件时,会直接这么写,代码如下:
use strict; use Mail::SendEasy ; my $mail = new Mail::SendEasy( smtp => 'smtp.ttlsa.com' , user => '[email protected]' , pass => 'QQ_qun:39514058', ); my $status = $mail->send( from => '[email protected]' , from_title => $mail_title , to => $mail_to , subject => $mail_subject , msg => $mail_msg , html => "<b>$mail_msg</b>" , ); if (!$status) { print $mail->error ;}这存在一个问题, 会长期阻塞在send函数,而无限制等待下去, 直到超时,很可能会拖垮服务器的。我们可以使用gearman,来改变这种发送邮件的方式。第一步,创建一个worker实例SENDMAIL,并向job server注册,等待接收任务并执行发送邮件的操作。第二步,客户端只需要将发送邮件的任务丢给job server便退出,没你什么事了。上代码:
#work_SENDMAIL.pl use strict; use Mail::SendEasy ; use Gearman::Worker; use JSON; use Data::Dumper; my $gearman_server='127.0.0.1:4730'; my $worker=new Gearman::Worker; $worker->job_servers($gearman_server); $worker->register_function(SENDMAIL=>\&sendmail); $worker->work while 1; sub sendmail{ my $job=shift; my $param=$job->arg; my $json = JSON->new->allow_nonref; my $hash_ref=$json->decode($param); my $addr=$hash_ref->{'email'}; my $subject=$hash_ref->{'subject'}; my $msg=$hash_ref->{'msg'}; my $mail = new Mail::SendEasy( smtp => 'smtp.ttlsa.com' , user => '[email protected]' , pass => 'QQ_qun:39514058', ); my $status = $mail->send( from => '[email protected]' , from_title => $subject , to => $addr , subject => $subject , msg => $msg , html => "<b>$msg</b>" , ); if (!$status) { print $mail->error ;} }
#gearman_client.pl use strict; use Gearman::Client; use JSON; my $gearman_server="127.0.0.1:4730"; my $worker='SENDMAIL'; my $data={}; $data->{'subject'} = "www.ttlsa.com --- Operation & Maintenance of Time To Live"; $data->{'msg'} = "Date: gettime() 如何记住本站点ttlsa.com拆开便是ttl(time to live)sa(system admin).com,本站点坚持每日更新,欢迎纠错、评论以及转载."; $data->{'email'} = '[email protected]'; my $json = JSON->new->allow_nonref; my $param=$json->encode($data); gearman_add_job($gearman_server,$worker,$param,2); sub gearman_add_job { my $server=shift; my $function_name=shift; my $function_param=shift; my $level=shift or 1; my $gearman_client = Gearman::Client->new; $gearman_client->job_servers($server); if($level == 1){ my $result = $gearman_client->do_task($function_name,\$function_param,{}); } elsif($level == 2){ my $result = $gearman_client->dispatch_background($function_name, \$function_param,{}); } } sub gettime { my @time=(localtime)[5,4,3,2,1,0]; $time[0]+=1900; $time[1]+=1; return sprintf("%04u-%02u-%02u %02u:%02u:%02u",@time); }
完成上面的改造不要认为解决了发送邮件会长期阻塞在send函数,而无限制等待下去的问题了。这其实只是解决了一部分,还有一个问题需要考虑进去,既然采用了异步方式,那么应用程序是不知道邮件是否发送成功的,因此需要记录任务执行的结果,可以将结果写入数据库,定期的对发送失败的邮件进行再次发送,或写个异常处理的worker,捕获发送邮件异常,进行多次尝试发送。
Gearman高级应用
需要小心的一件事情是数据的共享。Gearman 不进行所交换数据的任何转换或操作。对于这里使用的简单字符串和整数没有问题,但是不能共享 PHP 中的数组值并期望能在 Java 语言中被理解。对于这种类型的交互,可以使用很多结构化数据标准中的一种,比如 JavaScript Object Notation (JSON) 或 XML。另外,如果您在处理来自数据库的信息,只要共享 ID 或者找到需要处理的数据时要用到的信息即可,或者使用 memcached 这样的透明方法(尽管可能仍然需要 JSON 或等价物)。
gearman+mysql实现持久化队列
持久化队列是在0.6版本中新添的一项功能,允许将队列存放在drizzle或mysql中。0.7版本允许将队列存放在memcached。0.9版本可以将队列存放在sqlite3或postgresql。
在gearman job服务器内部,所有的job队列都是存放在内存中的,这就意味着一旦服务器重启或崩溃,未执行的job将会丢失而不会被worker服务器执行。持久化队列将后台作业存放在一个外部持久的队列中。持久化队列只对后台jobs有效,因为前台jobs依附于客户端。如果job服务器挡掉了,客户端会检测到,将会从其他地方重新启动这个前台job或者返回错误。而后台jobs没有依附于客户端,如果要想让它运行则需要提交。
创建数据库和表
create database gearman;
create table `gearman_queue` (
`unique_key` varchar(64) NOT NULL,
`function_name` varchar(255) NOT NULL,
`priority` int(11) NOT NULL,
`data` LONGBLOB NOT NULL,
`when_to_run` INT, PRIMARY KEY (`unique_key`)
);
启动gearmand
gearmand -q libdrizzle –libdrizzle-host=127.0.0.1 –libdrizzle-user=gearman –libdrizzle-password=password –libdrizzle-db=gearman –libdrizzle-table=gearman_queue –libdrizzle-mysql
创建一个后台job
gearman -f testqueue -b xx00
查看队列
select * from gearman.gearman_queue
执行队列中的job
gearman -f testqueue -w
查看队列
select * from gearman.gearman_queue //这条job删除掉了
假如,我直接向这张表里插入一条记录,队列里会多一条吗?哈哈,我试过了,不会,除非gearmand重启才能识别
注意:gearmand能支持哪些持久化方式,与源码编译时带的的库关系
源码编译
./configure --prefix=/usr/local/gearman --with-libevent-prefix=/usr/local/libevent
最后有个提示:
Configuration summary for gearmand version 0.14
* Installation prefix: /usr/local/gearman
* System type: unknown-linux-gnu
* Host CPU: x86_64
* C Compiler: gcc (GCC) 4.6.3
* Assertions enabled: yes
* Debug enabled: no
* Warnings as failure: no
* Building with libsqlite3 yes
* Building with libdrizzle no
* Building with libmemcached no
* Building with libpq no
* Building with tokyocabinet no
这里最后面的五行说明了支持哪些持久化方式,这个示例里支持libsqlite3,那么可以:
/usr/local/sbin/gearmand -l /usr/local/gearman/log/trace2.log --verbose INFO -p 4830 -q libsqlite3 --libsqlite3-db /usr/local/sqlite/bin/gearman --libsqlite3-table gearman_queue -d
很遗憾,我到现在也没能编译出对MySQL的持久化
队列持久化与异步发邮件相结合
在Gearman已配置好了持久化队列后,展示一个异步发邮件的功能。
先看看 client.php :
<?php
$client = new GearmanClient();
$client->addServer(); // 預設為 localhost
$emailData = array(
'name' => 'web',
'email' => '[email protected]',
);
$imageData = array(
'image' => '/var/www/pub/image/test.png',
);
$client->doBackground('sendEmail', serialize($emailData));
echo "Email sending is done.\n";
$client->doBackground('resizeImage', serialize($imageData));
echo "Image resizing is done.\n";
接下來我們要製作 Worker ,以下就是 worker.php :
<?php
$worker = new GearmanWorker();
$worker->addServer(); // 預設為 localhost
$worker->addFunction('sendEmail', 'doSendEmail');
$worker->addFunction('resizeImage', 'doResizeImage');
while($worker->work()) {
sleep(1); // 無限迴圈,並讓 CPU 休息一下
}
function doSendEmail($job)
{
$data = unserialize($job->workload());
print_r($data);
sleep(3); // 模擬處理時間
echo "Email sending is done really.\n\n";
}
function doResizeImage($job)
{
$data = unserialize($job->workload());
print_r($data);
sleep(3); // 模擬處理時間
echo "Image resizing is really done.\n\n";
}
準備好 Client 和 Worker 的程式後,就可以測試看看了。首先我們必須得先執行 worker.php ,讓它開始服務。
php worker.php
這時我們會看到 worker.php 停駐在螢幕上等待服務。
接著我們開啟另一個 console 視窗來執行 client.php :
php client.php
會立刻出現以下結果:
Email sending is done.
Image Resizing is done.
而切換到執行 worker.php 的 console 時,就會看到以下執行結果:
Array
(
[who_send] => web
[get_email] => [email protected]
)
Email sending is really done.
Array
(
[image] => /var/www/pub/image/test.png
)
Image resizing is really done.
這表示 Worker 正常地處理 Client 的需求了。
現在試著把 worker.php 停掉 (Ctrl+C) ,然後再執行 client.php ,大家應該會發現 client.php 還是正常地完成它的工作;這是因為 Job Server 幫我們把需求先放在 Queue 裡,等待 Worker 啟動後再處理。
這時可以查看 MySQL 的 gearman 資料庫,在 gearman_queue 資料表中應該就會看到以下結果:
Message Queue 這個架構的應用可以說相當廣泛,尤其在大流量的網站上,我們能透過它來來有效運用分散式的系統架構,以處理更多使用者的需求。
而目前 Gearman 可說是在 PHP 上一個很棒的 Message Queue 支援套件,而且 API 也相當完善;因此如果能善用 Gearman 的話,那麼我們在 PHP 網站的架構上就可以有更大的延展性,也能有更多的可能性。
有機會就去試試看吧!
使用MySQL UDFs来调用gearman分布式任务分发系统
当向表插入数据的时候,触发执行某些任务
一.安装gearman-mysql-udf
# apt-get install libmysql++-dev
# wget https://launchpad.net/gearman-mysql-udf/trunk/0.6/+download/gearman-mysql-udf-0.6.tar.gz
# tar zxvf gearman-mysql-udf-0.6.tar.gz
# ./configure –with-mysql=/usr/bin/mysql_config –libdir=/usr/lib/mysql/plugin
# make
# make install
# mysql
[codesyntax lang="sql"]
mysql> CREATE FUNCTION gman_do RETURNS STRING
SONAME “libgearman_mysql_udf.so”;
mysql> CREATE FUNCTION gman_do_high RETURNS STRING
SONAME “libgearman_mysql_udf.so”;
mysql> CREATE FUNCTION gman_do_low RETURNS STRING
SONAME “libgearman_mysql_udf.so”;
mysql> CREATE FUNCTION gman_do_background RETURNS STRING
SONAME “libgearman_mysql_udf.so”;
mysql> CREATE FUNCTION gman_do_high_background RETURNS STRING
SONAME “libgearman_mysql_udf.so”;
mysql> CREATE FUNCTION gman_do_low_background RETURNS STRING
SONAME “libgearman_mysql_udf.so”;
mysql> CREATE AGGREGATE FUNCTION gman_sum RETURNS INTEGER
SONAME “libgearman_mysql_udf.so”;
mysql> CREATE FUNCTION gman_servers_set RETURNS STRING
SONAME “libgearman_mysql_udf.so”;
mysql> SELECT gman_servers_set(“192.168.1.60:4730,192.168.1.60:4731″) as gman_servers; //设置gearman server
+————————————-+
| gman_servers |
+————————————-+
| 192.168.1.60:4730,192.168.1.60:4731 |
+————————————-+
mysql> create table udf_test(
-> id int unsigned auto_increment primary key,
-> val varchar(20) not null); //新建表
mysql> create trigger sendmail before insert on udf_test for each row set @return=gman_do_background(‘MAIL’,'undef’); //创建触发器,当向表udf_test插入数据时候,执行任务。
[/codesyntax]
# perl -MCPAN -e shell
cpan> install Gearman::Worker //安装Gearman::Worker模块
cpan> install Mail::SendEasy //安装Mail::SendEasy模块
# vi WORKER_SENDMAIL.pl //创建worker任务
[codesyntax lang="perl"] use strict; use Mail::SendEasy ; use v5.10; use Gearman::Worker; my $worker=new Gearman::Worker; $worker->job_servers(’192.168.1.60:4730′); $worker->register_function(MAIL=>\&sendmail); $worker->work while 1; sub sendmail{ my $job=shift; my $date=localtime; my $mail = new Mail::SendEasy( smtp => ‘smtp.ttlsa.com’ , user => ‘[email protected]’ , pass => ‘******’, ); print “$date\n”; my $status = $mail->send( from => ‘[email protected]’ , from_title => ‘ttlsa’ , to => ‘[email protected]’ , subject => “MAIL Test $date” , msg => “$date” , html => “<b>test $date</b>” , ); if (!$status) { print $mail->error ;} } [/codesyntax]# perl WORKER_SENDMAIL.pl &
2.查看是否收到邮件
三.Gearman server信息
通过Gearman实现MySQL到Redis的数据同步
对于变化频率非常快的数据来说,如果还选择传统的静态缓存方式(Memocached、File System等)展示数据,可能在缓存的存取上会有很大的开销,并不能很好的满足需要,而Redis这样基于内存的NoSQL数据库,就非常适合担任实时数据的容器。
但是往往我们又有数据可靠性的需求,采用MySQL作为数据存储,不会因为内存问题而引起数据丢失,同时也可以利用关系数据库的特性实现很多功能。
所以就会很自然的想到是否可以采用MySQL作为数据存储引擎,Redis则作为Cache。而这种需求目前还没有看到有特别成熟的解决方案或工具,因此本文将尝试采用Gearman+PHP+MySQL UDF的组合异步实现MySQL到Redis的数据复制。
MySQL到Redis数据复制方案
无论MySQL还是Redis,自身都带有数据同步的机制,像比较常用的MySQL的Master/Slave模式,就是由Slave端分析Master的binlog来实现的,这样的数据复制其实还是一个异步过程,只不过当服务器都在同一内网时,异步的延迟几乎可以忽略。
那么理论上我们也可以用同样方式,分析MySQL的binlog文件并将数据插入Redis。但是这需要对binlog文件以及MySQL有非常深入的理解,同时由于binlog存在Statement/Row/Mixedlevel多种形式,分析binlog实现同步的工作量是非常大的。
因此这里选择了一种开发成本更加低廉的方式,借用已经比较成熟的MySQL UDF,将MySQL数据首先放入Gearman中,然后通过一个自己编写的PHP Gearman Worker,将数据同步到Redis。比分析binlog的方式增加了不少流程,但是实现成本更低,更容易操作。
通过MySQL UDF + Trigger同步数据到Gearman
MySQL要实现与外部程序互通的最好方式还是通过MySQL UDF(MySQL user defined functions)来实现。为了让MySQL能将数据传入Gearman,这里使用了lib_mysqludf_json和gearman-mysql-udf的组合。
安装lib_mysqludf_json
使用lib_mysqludf_json的原因是因为Gearman只接受字符串作为入口参数,可以lib_mysqludf_json通过将MySQL中的数据编码为JSON字符串
apt-get install libmysqlclient-dev
wget https://github.com/mysqludf/lib_mysqludf_json/archive/master.zip
unzip master.zip
cd lib_mysqludf_json-master/
rm lib_mysqludf_json.so
gcc $(mysql_config --cflags) -shared -fPIC -o lib_mysqludf_json.so lib_mysqludf_json.c
#这里的mysql_config是mysql的一个程序,一般在mysql安装目录的/usr/bin目录下
可以看到重新编译生成了 lib_mysqludf_json.so 文件,此时需要查看MySQL的插件安装路径:
mysql -u root -pPASSWORD --execute="show variables like '%plugin%';"
+---------------+------------------------+
| Variable_name | Value |
+---------------+------------------------+
| plugin_dir | /usr/lib/mysql/plugin/ |
+---------------+------------------------+
然后将 lib_mysqludf_json.so 文件复制到对应位置:
cp lib_mysqludf_json.so /usr/lib/mysql/plugin/
最后登入MySQL运行语句注册UDF函数:
CREATE FUNCTION json_object RETURNS STRING SONAME 'lib_mysqludf_json.so';
安装gearman-mysql-udf
方法几乎一样:
apt-get install libgearman-dev
wget https://launchpad.net/gearman-mysql-udf/trunk/0.6/+download/gearman-mysql-udf-0.6.tar.gz
tar -xzf gearman-mysql-udf-0.6.tar.gz
cd gearman-mysql-udf-0.6
./configure --with-mysql=/usr/bin/mysql_config --libdir=/usr/lib/mysql/plugin/
make && make install
登入MySQL运行语句注册UDF函数:
CREATE FUNCTION gman_do_background RETURNS STRING SONAME 'libgearman_mysql_udf.so';
CREATE FUNCTION gman_servers_set RETURNS STRING SONAME 'libgearman_mysql_udf.so';
最后指定Gearman服务器的信息:
SELECT gman_servers_set('127.0.0.1:4730');
通过MySQL触发器实现数据同步
最终同步那些数据,如何同步,还是需要根据实际情况决定,比如我希望将数据表data的数据在每次更新时同步,那么编写Trigger如下:
DELIMITER $$
CREATE TRIGGER datatoredis AFTER UPDATE ON data
FOR EACH ROW BEGIN
SET @ret=gman_do_background('syncToRedis', json_object(NEW.id as `id`, NEW.volume as `volume`));
END$$
DELIMITER ;
尝试在数据库中更新一条数据查看Gearman是否生效。
需要说明一下,这里的 gman_do_background函数调用,与gearman的一次通讯是短连接,函数调用时连接,调用结束后连接就短掉, 如果并发压力比较大,需要考虑一下
Gearman PHP Worker将MySQL数据异步复制到Redis
Redis作为时下当热的NoSQL缓存解决方案无需过多介绍,其安装及使用也非常简单:
apt-get install redis-server pecl install redisecho "extension=redis.so" > /etc/php5/conf.d/redis.ini
然后编写一个Gearman Worker:redis_worker.php
#!/usr/bin/env php <? $worker = new GearmanWorker(); $worker->addServer(); $worker->addFunction('syncToRedis', 'syncToRedis'); $redis = new Redis(); $redis->connect('127.0.0.1', 6379); while($worker->work()); function syncToRedis($job) { global $redis; $workString = $job->workload(); $work = json_decode($workString); if(!isset($work->id)){ return false; } $redis->set($work->id, $workString); }