以往在开发会员注册功能时,通知信总是即时寄出;虽然寄信这个动作不会花太多时间,但遇到网路塞车或是邮件伺服器反应较慢时,那麼会员就有可能要等好一阵子才能进到下一个网页。

那麼我们要怎麼解决这种问题呢?

如果大家有用过 SlideShare 或是 YouTube 的话,这些网路服务在我们上传档案后,就会开始做转换的动作,但却又不必让我们在那里傻傻的等;那麼同样的道理,如果我们能把寄信这个讯息丢到一个负负发信的机器去,然后就继续处理我们的工作,而不必等候它的通知,这样不就解决我们的问题了吗?

这个技巧就称为「 Message Queue 」。

Message Queue 原理

想像一下我们人现在正在银行,现在柜台窗口的办事人员都在忙碌,而门口的服务人员会亲切地给我们票卡,让我们在一旁稍等;这时我们可以先看看报纸,或是打电话先到公司交代一些事情,等待有空的窗口呼叫我们就可以了。

用程式的角度来说,当我们发出请求之后并不一定马上就要处理,而是先进入伫列等候,这时我们就可以先行往下执行其他步骤;当可以处理该需求的资源有空闲时,就会帮我们做处理。这种模式就是 Message Queue 的基本概念。

所以在 Message Queue 中就有以下这三个角色:

Client :就是需要服务的客户;也就是发送需求的程式。

Job Server :就是整间银行,更严格的来说,它指的是「了解客户需要何种服务,并查看哪个窗口可以处理这项需求」的机制。一般来说,在系统里它通常会是个 Daemon 。

Worker :负责处理客户需求的柜台人员;也就是实际处理需求的程式。

我们简单用下图来说明:

首先,执行 Message Queue 服务的 Job Server 可以是多台伺服器组成,也就是分散式架构。然后我们会在 Job Server 上注册并执行 Worker 程式,这些 Worker 程式会一直循环地等候,直到 Job Server 呼叫它执行工作。

在 Client 发送出需求之后,会将要需要的资料及动作记录在 Job Server 上,这时 Job Server 会查看是否有空闲并且符合需求的 Worker ;例如 Client 程式告诉 Job Server 要寄信,那麼 Job Server 就会查看负责寄信的 Worker 目前是否有空。当 Worker 有空时,那麼 Job Server 就会从伫列中把 Client 的需求转移给 Worker 开始执行。

在 Worker 结束工作后,也会发送通知给 Job Server ,这时 Job Server 就会视状况把结果回传给 Client 。也就是这样的机制,让 Client 不必再等候需求的执行结果,而可以直接再往下执行其他动作。

值得注意的是,一般 Client 和 Job Server 的主机会是分开的;这样的架构,才不会造成执行 Client 程式主机的负担。不过稍后的示范里,我们会在同一台主机上实作 Client 和 Worker 。

Gearman 简介

实作 Message Queue 套件有很多, Gearman 也是其中之一。它的详细历史与介绍请参考官方说明,以下我们简单介绍它的应用方式:

下图取自官方手册,主要是说明 Gearman 的运作机制:

蓝色部份是由我们开发的程式码,而黄色部份是由 Gearman 或第三方 API 提供的。也因为只要符合 API 规范就可以跟 Gearman 沟通,所以 Client 和 Worker 并不需要用同样的语言来实作 API ;例如我们可以在 Client 端使用 PHP 开发程式,在 Worker 端使用 C 或 Perl 来开发,因为它们有提供 Gearman 的 API 来供我们呼叫。

注:在官方网站的下载页中,可以看到分别以各种语言实作的 Gearman API Library 。

另外 Gearman 也提供了 Persistent Queues 的功能,也就是当 Worker 在无法提供服务时, Job Server 会将 Queue 保留 Persistent Storage 中,以便在 Worker 恢复运作时能再次运行。

安装 Gearman Job Server

Gearman 在官方网站上已经提供了各种套件版本的安装说明,不过目前不论是 Server 端或 Client API 端,都不提供 Windows 版本,因此以下的安装与范例我都将以 Ubuntu 10.04 为主。

注:这里我也假设大家已经安装好了 PHP 。

首先我们要安装 add-apt-repository 这个工具,它可以帮我们管理 Ubuntu 的 Package Repository :

sudoaptitudeinstall python-software-properties

接下来透过安装好的 add-apt-repository 来加入新的 repository :

sudo add-apt-repository ppa:gearman-developers/ppasudoaptitude update

设定好 repository 后,就可以找到 gearman 及相关套件,然后用以下的指定安装它们:

sudoaptitudeinstall libevent-dev gearman libgearman2 libgearman-dev libdrizzle0

其中 libdrizzle 套件就是提供 Gearman 的 Persistent Queues 功能,稍后我们会使用它来串接 MySQL 。

完成后, Gearman Job Server 就会在我们的系统中启动了。

设定 Persistent Queue

接著我们要在 MySQL 中先建立一个 gearman 资料库,这样稍后启动 Gearman Job Server 时,才能建立所需要的资料表:

echo'CREATE DATABASE gearman'>/tmp/temp.sql ; mysql -u root -p</tmp/temp.sql ; rm-f/tmp/temp.sql

而为了让 Gearman Job Server 能够串接 MySQL ,我们要在 Service Script 中设定相关参数。编辑 gearman-job-server 指令稿:

sudovi/etc/init.d/gearman-job-server

找到第一个 $PARAMS ,将它置换为:

--queue-type=libdrizzle --libdrizzle-host=127.0.0.1 --libdrizzle-user=root --libdrizzle-password=123456--libdrizzle-db=gearman --libdrizzle-table=gearman_queue --libdrizzle-mysql

其中 libdrizzle-host 可换成各位惯用的 MySQL 伺服器 IP ,而 libdrizzle-user 、 libdrizzle-password 则是要有 CREATE TABLE 的权限。

注:理论上应该是把上面的参数设定到 Script 的 PARAM="…" 中就可以了,但不晓得为什麼我实地测试的结果却无法成功。感谢网友指正,再把 test -f /etc/default/gearman-job-server && . /etc/default/gearman-job-server 这行注解即可;或是直接修改 /etc/default/gearman-job-server 这个档案也可以。

最后重新启动 Gearman Job Server :

sudo service gearman-job-server stopsudo service gearman-job-server start

我们可以用 ps 指令来查看启动是否成功:

ps aux |grep gearman

出现以下结果的话,就表示我们成功安装并设定好 Gearman Job Server 了。

gearman 85630.10.280961512 ? Ss 19:480:00 /usr/sbin/gearmand --pid-file=/var/run/gearman/gearmand.pid --user=gearman --daemon--log-file=/var/log/gearman-job-server/gearman.log --queue-type=libdrizzle --libdrizzle-host=127.0.0.1 --libdrizzle-user=root --libdrizzle-password=xxxxxx --libdrizzle-db=gearman --libdrizzle-table=gearman_queue --libdrizzle-mysqlxxxxxx 85660.00.13324796 pts/0 S+ 19:480:00 grep--color=auto gearman
安装 PHP Gearman API Extension

因为后面的范例是使用 PHP 做示范,所以我们也要透过 PECL 来协助我们安装 Extension 。以下的指令会帮我们建立好  PHP 的 PECL  环境:

sudoaptitudeinstall php5-dev php-pear

安装好 PECL 之后,就可以透过 pecl 指令来安装 Gearman Extension 了:

sudo pecl install channel://pecl.php.net/gearman-0.7.0

上面的指令执行成功之后,我们要在 php.ini 中启动 gearman ,所以输入以下指令以编辑 php.ini :

sudovi/etc/php5/cli/php.ini

在 php.ini 的最后面加入以下两行:

[gearman]extension=gearman.so

注:这里编辑的是 CLI 环境下的 php.ini ,如果是 Apache 下的,请改为编辑 /etc/php5/apache2/php.ini

简易实作

接下来,我们可以试著用 PHP API 来连接 Job Server 。前面安装好 PECL 的 Gearman Extension 后,我们就可以在 PHP 程式里建立操作 Gearman API 的物件了。

以下我用简单的方式来模拟 Client 和 Worker 的运作,所以这里 Client 和 Worker 会在同一部主机上,但实际运作时是不需要的,请大家注意。

Client 端程式

先看看 client.php :

<?php$client=new GearmanClient();$client->addServer();// 预设为 localhost $emailData=array('name'=>'web','email'=>'member@example.com',); $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";

首先, PHP Gearman Extension 提供了一个名为 GearmanClient 的类别,它可以让程式安排工作给 Job Server 。

addServer 方法表示要通知的是哪些 Job Server ,也就是说如果有多台 Job Server 的话,就可以透过 addServer 新增。

然后我们将要呼叫哪个 Worker 以及该 Worker 所需要的资料,利用 GearmanClient 的 doBackground 方法传送过去。 doBackground 方法顾名思义就是在背景执行, Client 在丢出需求后就可以继续处理其他的程式,也就是我们常说的「射后不理」。

doBackground 方法的第一个参数是告诉 Job Server 要执行哪个功能,而这个功能则是由 Worker 提供的;要注意是,这个参数只是识别用的,并不是真正的函式名称。而第二个参数是要传给 Worker 的资料,它必须是个字串;因此如果要传送的是阵列的话,我们就要用 PHP 的 serialize 函式来对这些资料做序列化。

Worker 端程式

接下来我们要制作 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";}

PHP 的 Gearman Extension 也提供了一个 GearmanWorker 类别,让我们可以实作 Worker 。而 GearmanWorker 类别也提供了 addServer 方法,让所生成的 Worker 物件可以注册到 Job Server 中。

另外 GearmanWorker 类别也提供了 addFuncton 方法,告诉 Job Server 自己可以处理哪些工作。 addFunction 的第一个参数就是对应到 GearmanClient::doBackground 方法的第一个参数,也就是功能名称;这使得 Client 和 Worker 能透过这个名称来互相沟通。而第二个参数则是一个 callback 函式,它会指向真正应该要处理该工作的函式或类别方法等。

最后因为 Worker 因为要随时准备服务,是不能被中断的,因此我们透过一个无限回圈来让它常驻在 Job Server 中。

测试

准备好 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] => member@example.com)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 资料表中应该就会看到以下结果:

这表示 Job Server 成功地将 Queue 保留在 MySQL 资料表中。

接著再执行 worker.php ,这时 Job Server 会得知 Worker 复活,赶紧将 Queue 里面属於该 Worker 应该执行的工作再发送出去以完成作业;而 Worker 完成作业后, Job Server 就会把 Queue 清空了。

是不是很有趣呢?

心得

Message Queue 这个架构的应用可以说相当广泛,尤其在大流量的网站上,我们能透过它来来有效运用分散式的系统架构,以处理更多使用者的需求。

而目前 Gearman 可说是在 PHP 上一个很棒的 Message Queue 支援套件,而且 API 也相当完善;因此如果能善用 Gearman 的话,那麼我们在 PHP 网站的架构上就可以有更大的延展性,也能有更多的可能性。

转之:http://www.jaceju.net/blog/archives/1211