作者归档:admin

PHP队列开发之Beanstalk

Beanstalk是一个基于内存的(binlog持久化到硬盘),事件驱动(libevent),简单、快速的任务队列,支持大部分编程语言,将前台的任务转为后台异步处理,为web开发提供更高弹性。它可以支持多个server(客户端支持),一个任务只会被投递到一台server,一个任务只会被一个消费者获取(Reverse)。

相比RabbitMQ,Beanstalk作为一个任务队列,设计比较简单,支持以下特性:

  • 优先级(priority),可以对任务进行优先处理(或降级),越小的值优先级越高(0~4,294,967,295),默认按先进先出(FIFO)
  • 延迟执行(delay),一个任务创建完成并稍后再执行(比如等待主从同步)
  • 超时重试(TTR),一个任务没有在指定时间内完成,将会被重新投递,由其他客户端处理。客户端也可以主动进行延时(touch)或重新入队(release)
  • 隐藏(bury),一个任务执行失败了,可以先隐藏,隐藏的任务可以被重新激活(kick);

一个任务如果没有被删除,那么它就可以被重新获取。下面是大多数任务的生命周期:

   put with delay               release with delay
  ----------------> [DELAYED] <------------.
                        |                   |
                        | (time passes)     |
                        |                   |
   put                  v     reserve       |       delete
  -----------------> [READY] ---------> [RESERVED] --------> *poof*
                       ^  ^                |  |
                       |   \  release      |  |
                       |    `-------------'   |
                       |                      |
                       | kick                 |
                       |                      |
                       |       bury           |
                    [BURIED] <---------------'
                       |
                       |  delete
                        `--------> *poof*

CentOS下安装Beanstalkd

sudo yum install beanstalkd
#启动beanstalk
sudo service beanstalkd start
#beanstalkd -l 192.168.33.14 -p 11300

PHP下面有个C扩展beanstalk库可以使用,基于libbeanstalkclient

git clone https://github.com/bergundy/libbeanstalkclient.git
cd libbeanstalkclient
mkdir m4
#开始编译
sudo ./autogen.sh

#创建libbeanstalkclient.conf,内容为/usr/lib
sudo vim /etc/ld.so.conf.d/libbeanstalkclient.conf
#使配置生效
sudo ldconfig

git clone https://github.com/nil-zhang/php-beanstalk.git
cd php-beanstalk
phpize
./configure
sudo make
sudo make install
sudo vim /etc/php.ini

编辑php.ini增加以下内容

[beanstalk]
extension = "beanstalk.so"

查看是否加载成功

php -m
#加载成功则重启php-fpm
sudo service php-fpm restart

PHP测试代码

<?php
    $bsc = new Beanstalk();

    $bsc->addserver("192.168.33.14", 11300);
    $bsc->addserver("192.168.33.12", 11300);

    $tubes = $bsc->list_tubes();
    print_r($tubes);

    for($i = 0; $i < 10; $i++)
    {
        $key = "key".$i;
        $value = "value".$i;

        $bsc->use($key);
        $bsc->put($key, $value);
        echo "$key\t$value\n";

        $bsc->watch($key);
        $job = $bsc->reserve($key);
        print_r($job);

        if($bsc->bury($job['id'], $key))
            echo "bury ok\n";
        else
            echo "bury failed\n";

        $bsc->kick(100, $key);
        if($bsc->delete($job['id'], $key))
            echo "delete ok\n";
        else
            echo "delete failed \n";

        $bsc->ignore($key);
        echo "\n";
    }

    echo "done\n";

注意由于Beanstalk服务端实现的比较简单,协议特性需要客户端支持,不同的实现可能效果不一样,这个客户端并没有实现延时发送(delay),超时重试(TTR)。需要这些特性建议使用这个库:PHP Beanstalkd。前台生产者创建任务:

<?php
include 'lib/Beanstalk.php';
$bean = Beanstalk::init();
$bean->addServer('192.168.33.14', 11300);
$bean->addServer('192.168.33.12', 11300);
$bean->useTube('my-tube');
$bean->put('Hello World!', 1024);
$bean->put('Hello World!2', 1023);
$bean->put(json_encode(array('what','how')), 1000, 1, 1);

后台消费者处理任务

include 'lib/Beanstalk.php';
$bean = Beanstalk::init();
$bean->addServer('192.168.33.12', 11300);
$bean->addServer('192.168.33.14', 11300);
$bean->watchTube('my-tube');

while (true)
{
	try
	{
		$job = $bean->reserve($timeout = 10);

		/* process job ... */
		var_dump($job);
		//var_dump($job->getMessage());

		$job->delete();
	}
	catch (BeanstalkException $e)
	{
		switch ($e->getCode())
		{
			case BeanstalkException::TIMED_OUT:
				echo "Timed out waiting for a job.  Retrying in 1 second.";
				sleep(1);
				continue;
				break;
			default:
				throw $e;
				break;
		}
	}
}

注意:客户端获取任务(reverse)是阻塞的(blocking),直到超时;同一个队列(tube)的任务按FIFO进行处理(除非指定优先级);任务内容长度不能超过65536;作为内存队列需要注意是否会内存超出,可以快速处理到Mysql。

使用Beanstalk任务队列提升PHP异步处理能力,降低程序耦合度,使前台更专注,后台处理耗时、扩展性任务(也可以使用其他语言开发),使得web架构更具扩展性。

参考链接:
Scalable Work Queues with Beanstalk
Beanstalk Protocol
Frequently Asked Questions for beanstalkd
Getting Started with Beanstalkd
Queue your work
Asynchronous Processing in Web Applications, Part 2: Developers Need to Understand Message Queues

PHPPHP 7

最近学习了PHP 7介绍的视频,看到了PHP每一天都在变好。PHP 将会带来许多新的特性:

  • 抽象语法树,在编译opcode时能够做更多的优化
  • INT64提升,支持大于2GB的字符串/文件上传
  • 标量类型声明,可以声明参数类型
  • 返回值声明,可以声明返回值类型
  • 新操作符,<=>操作符,大于返回1,等于返回0,小于返回-1
  • 统一变量语法;
  • 引擎异常改进,函数不存在的fata error转为异常可以捕获处理

等等。
除了新特性外,PHP 7使用了新引擎PHP NG,性能得到了巨大的提升(与HHVM对比)。
这里的性能优化比较有意思, 非常值得借鉴学习。为什么要进行重构:

  • PHP 5.4之后php引擎的性能提升已经不大,Zend VM已经高度优化
  • PHP JIT(Just In Time Compiler)项目在benchmakr上性能提升巨大(8倍),但是在实际业务提升不大(离开实际业务谈benchmark都是耍流氓)
  • 分析wordpress项目发现瓶颈并不在Zend VM上面,21% CPU时间花在了内存管理,12%时间花在了hashTable操作,30% CPU时间花在了内部调用,25% CUP时间花在了VM

首先应当分析实际业务的性能消耗占比,确定需要优化的地方。JIT仅仅优化了Zend VM,提升是有限的,需要对其他地方(内存操作,HashTable,函数调用)也进行优化,所以才进行的重构。

PHP NG在很多方面做了优化:

  • 标量类型不做引用计数,zval分配在堆上,增加Referance类型
  • 增加类型zend string,将字符串与值连续分配,避免CPU cache miss,避免二次读内存
  • 原本的HashTable变为zend array,数组值为zval,内存一次分配,提升data locality,避免CPU cache miss
  • 函数调用优化,减少重复入栈
  • 参数解析优化,将不确定转化为确定
  • zend_qsort排序优化
  • 内存管理优化,减少CPU cache miss
  • 常用函数优化
  • 字符串拼接优化,减少内存访问次数

通过这一系列的优化,改进了PHP的内存操作,改进了HashTable操作,改进了内部函数调用,降低了CPU 时间消耗,这时候再来做PHP JIT优化就有意义。PHP 7接下来仍然会继续优化,迁移常用扩展。
许多优化都是微小的,但是积累起来后却是巨大,特别是对于微博这样的网站,1%的性能提升都意义非凡。
优化是具有专向性的,对A场景的优化可能并不适合B场景,所以需要分析实际的业务中的瓶颈和调用频次,权衡优化的方向。
现在距离10月份发布已经不远了,大家也可以检查自己项目是否兼容PHP 7,性能提升多少。

参考链接:
PHP7 – New engine for good old train
PHP7 VS HHVM (WordPress)
PHPNG (next generation)
What to Expect When You’re Expecting: PHP 7, Part 1
PHP 7 Feature Freeze
HHVM
HHVM 是如何提升 PHP 性能的?
PHP Fights HHVM and Zephir with PHPNG
rlerdorf/php7dev
Building and testing the upcoming PHP7
Install PHP 7 on Ubuntu 14.04

PHP yield应用

PHP 5.5开始新增了神奇的关键字yield,能够从生成器(generators)中返回数据。yield有点像普通函数中的关键字return,但是不会彻底停止函数的执行(普通函数一旦return便不执行了),可以暂停循环并返回值,每一次调用便从中断处继续迭代。生成器可以用于替代循环迭代,每一次调用返回一个生成器对象(generator)

yield能够延迟执行,可以用于对大量数据进行迭代而不用预先在内存中生成数组。例如动态生成一个大数组:

<?php
function xrange($start, $end, $step = 1) {
    for ($i = $start; $i <= $end; $i += $step) {
        yield $i;
    }
}

foreach (xrange(1, 1000000) as $num) {
    echo $num, "\n";
}

$range = xrange(1, 1000000);
var_dump($range); // object(Generator)#1
var_dump($range instanceof Iterator); // bool(true)

利用yield简便、高效的生成fibonacci数列而不是循环或递归

<?php
function fibonacci($count) {
    $prev = 0;
    $current = 1;

    for ($i = 0; $i < $count; ++$i) {
        yield $prev;
        $next = $prev + $current;
        $prev = $current;
        $current = $next;
    }
}

foreach (fibonacci(48) as $i => $value) {
    echo $i , ' -> ' , $value, PHP_EOL;
}

利用yield来循环读取文件,而不需要像file函数那样一次性加载进来,节省内存

<?php
function file_lines($filename) {
    $file = fopen($filename, 'r'); 
    while (($line = fgets($file)) !== false) {
        yield $line; 
    } 
    fclose($file); 
}
 
foreach (file_lines('somefile') as $line) {
    // do some work here
}

yield除了能够返回值,用作变量时还可以接收值。

<?php
function logger($fileName) {
    $fileHandle = fopen($fileName, 'a');
    while (true) {
        fwrite($fileHandle, yield . "\n");
    }
}

$logger = logger(__DIR__ . '/log');
$logger->send('Foo');
$logger->send('Bar');

由于yield具有中断执行后再次调用又可以从中断处执行,外界又可以通过生成器对象(generator)的send方法进行交互,可以用于协程(coroutine),作多任务协作的流程控制。这里有个例子

<?php
class Task {
    protected $taskId;
    protected $coroutine;
    protected $sendValue = null;
    protected $beforeFirstYield = true;

    public function __construct($taskId, Generator $coroutine) {
        $this->taskId = $taskId;
        $this->coroutine = $coroutine;
    }

    public function getTaskId() {
        return $this->taskId;
    }

    public function setSendValue($sendValue) {
        $this->sendValue = $sendValue;
    }

    public function run() {
        if ($this->beforeFirstYield) {
            $this->beforeFirstYield = false;
            return $this->coroutine->current();
        } else {
            $retval = $this->coroutine->send($this->sendValue);
            $this->sendValue = null;
            return $retval;
        }
    }

    public function isFinished() {
        return !$this->coroutine->valid();
    }
}

class Scheduler {
    protected $maxTaskId = 0;
    protected $taskMap = []; // taskId => task
    protected $taskQueue;

    public function __construct() {
        $this->taskQueue = new SplQueue();
    }

    public function newTask(Generator $coroutine) {
        $tid = ++$this->maxTaskId;
        $task = new Task($tid, $coroutine);
        $this->taskMap[$tid] = $task;
        $this->schedule($task);
        return $tid;
    }

    public function schedule(Task $task) {
        $this->taskQueue->enqueue($task);
    }

    public function run() {
        while (!$this->taskQueue->isEmpty()) {
            $task = $this->taskQueue->dequeue();
            $task->run();

            if ($task->isFinished()) {
                unset($this->taskMap[$task->getTaskId()]);
            } else {
                $this->schedule($task);
            }
        }
    }
}
function task1() {
    for ($i = 1; $i <= 10; ++$i) {
        echo "This is task 1 iteration $i.\n";
        yield;
    }
}

function task2() {
    for ($i = 1; $i <= 5; ++$i) {
        echo "This is task 2 iteration $i.\n";
        yield;
    }
}

$scheduler = new Scheduler;

$scheduler->newTask(task1());
$scheduler->newTask(task2());

$scheduler->run();

有些任务是需要交互进行的,如socket的监听和回复;有些任务异步执行又需要回调,如A执行不阻塞程序,但B执行又取决于A是否执行完毕。这些都可以使用yield来进行封装,达到流程控制的目的。

参考链接:
Generators overview
What does yield mean in PHP?
What is the difference between a generator and an array?
Cooperative multitasking using coroutines (in PHP!)
What Generators Can Do For You
Generators and Coroutines in PHP
Generators in PHP
协程与yield
http://www.hitoy.org/coroutine-and-yield.html
Co-operative PHP Multitasking
Generator (computer programming)
异步处理在分布式系统中的优化作用

PHP异步IO

为了能更快的响应请求,耗时任务的执行不能阻塞当前脚本执行,而是放在最后执行,比如fastcgi_finsh_request。现在又多了一个方法,PHP也可以进行异步IO处理了。

PHP扩展eio是基于C语言的libeio库中开发的异步IO处理,可用于异步文件读写,自定义异步任务执行。Nodejs使用的libuv库封装了libeio和libev(libev也有对应的PHP扩展:ev),前者提供异步IO,后者提供高性能的事件驱动,进行高性能的请求的处理。另外,PHP异步多线程框架Swoole也提供了异步IO处理的能力。

在CentOS下编译安装PHP eio扩展:

wget https://pecl.php.net/get/eio-1.2.5.tgz
tar -zxvf eio-1.2.5.tzg
cd eio-1.2.5
phpize
./configure
sudo make
sudo make install

更改php.ini添加扩展

[eio]
extension=eio.so

查看是否安装成功
eio0
注意:eio扩展必须在socket扩展之后加载,否则会提示’ undefined symbol: php_sockets_le_socket in Unknown on line 0’。

PHP eio支持libeio的所有操作,当调用eio_poll/eio_event_loop时,触发IO处理,处理完成进行回调;未主动调用eio_poll/eio_event_loop则不阻塞当前程序执行,等待脚本执行结束后在提交异步处理。使用eio_customeio_nop可以自定义异步任务调用。

以下以写文件进行比较,示例一是常规的文件写操作:

$strBasePath = dirname(__FILE__);
$strFileName = $strBasePath.'/file.log';
$strContent = 'something to log ...';

$nStart = microtime(true);
$rFile = fopen($strFileName ,'a+');
for($i=0;$i<1000;$i++){
	$nLength = fwrite($rFile, $strContent);
}
fclose($rFile);
echo "done\r\n";

register_shutdown_function(function(){
	global $nStart;
	echo microtime(true) - $nStart;
	echo "\r\n";
});

示例二是PHP eio的异步写操作:

$strBasePath = dirname(__FILE__);
$strFileName = $strBasePath.'/file.log';
$strContent = 'something to log ...';
$nLenth = strlen($strContent);

function eioOpenCallBack($p_mxData ,$p_mxResult){
	//echo "Open.\r\n";
	if ($p_mxResult > 0) {
		eio_fstat($p_mxResult, EIO_PRI_DEFAULT, "eioStatCallBack", $p_mxResult);
	}
}
function eioStatCallBack($p_mxData ,$p_mxResult){
	global $strContent ,$nLenth;
	//echo "Stat.\r\n";
	if ($p_mxResult > 0) {
		eio_write($p_mxData, $strContent, $nLenth, $p_mxResult['size'], EIO_PRI_DEFAULT, "eioWriteCallBack", $p_mxData);
	}
}
function eioWriteCallBack($p_mxData ,$p_mxResult){
	//echo "Write.\r\n";
	if ($p_mxResult > 0) {
		eio_close($p_mxData, EIO_PRI_DEFAULT, "eioCloseCallBack", $p_mxData);
	}
}
function eioCloseCallBack($p_mxData ,$p_mxResult){
	//echo "Close.\r\n";
	if($p_mxResult == 0){
		//echo "End\r\n";
	}
}

$nStart = microtime(true);
for($i=0;$i<1000;$i++){
	eio_open($strFileName, EIO_O_CREAT | EIO_O_RDWR, EIO_S_IRUSR | EIO_S_IWUSR,EIO_PRI_DEFAULT, "eioOpenCallBack", $strFileName);
	
	//echo "Begin\r\n";
	
	//eio_event_loop();
}
echo "done\r\n";
register_shutdown_function(function(){
	global $nStart;
	echo microtime(true) - $nStart;
	echo "\r\n";
});

示例三是swoole扩展的异步写操作

$strBasePath = dirname(__FILE__);
$strFileName = $strBasePath.'/file.log';
$strContent = 'something to log ...';

$nStart = microtime(true);
for($i=0;$i<1000;$i++){
	swoole_async_write($strFileName ,$strContent);
}
echo "done\r\n";
register_shutdown_function(function(){
	global $nStart;
	echo microtime(true) - $nStart;
	echo "\r\n";
});

对比情况
eio1
可以看出,eio和swoole提交异步IO处理后,处理非常快,并未阻塞当前进程运行;eio更像是只花了循环的时间。swoole只能运行于CLI模式,使用了Linux Native AIO,处理非常快,写法也比较简单。eio可以运行于CGI和CLI模式,提交异步处理后会创建至多4个线程(eio_nthreads)进行处理,处理完成后仍然会返回主线程,所以PHP脚本执行结束后,主线程仍然会在在那里等待。eio的异步处理通过回调进行保证,写法上更加复杂。这就需要对异步回调进行包装,提供类似同步的代码,参见协程

参考链接:
PHP异步执行长时间任务
PHP的生命周期
libeio源码学习
PHP新增eio扩展,可以写类似node.js一样的异步IO了
深入浅出Node.js(五):初探Node.js的异步I/O实现
异步AIO的研究
关于C10K、异步回调、协程、同步阻塞
协程
linux AIO (异步IO) 那点事儿
什么程序设计语言机制是处理异步 IO 最恰当的抽象?
nodejs异步IO的实现
向facebook学习,通过协程实现mysql查询的异步化
Python 中的进程、线程、协程、同步、异步、回调
一个“蝇量级” C 语言协程库

CentOS上安装Nginx+PHP-FPM

Nginx是一个高性能、轻量级的HTTP服务器,占用内存少,稳定性高,也可做为反向代理服务器。Nginx采用事件驱动,在Linux操作系统下,Nginx使用epoll事件模型,充分使用异步逻辑,削减了上下文调度开销,并发服务能力更强。

PHP-FPM是PHP的FastCGI 进程管理器,具有一些高级特性,将PHP进程管理与Web服务器分开,可以分开部署在不同机器上,监听不同的端口和使用不同的 php.ini 配置文件。

PHP-FPM特性之一:fastcgi_finish_request能够在请求完成和刷新数据后,继续在后台执行耗时的工作(录入视频转换、统计处理等),达到分离执行后台任务而又不阻塞PHP快速响应,可作为PHP性能优化点。

PHP-FPM另一特性:可以记录请求当中慢响应的日志,类似Mysql满日志,包括文件名,函数名,行号等,可用于分析PHP的性能问题。

在Apache + mod_php下,每一个请求,Apache便会启动一个进程并加载php解释器来处理。在Nginx + PHP-FPM下,Nginx将PHP请求和环境变量通过socket传递给FastCGI进程;将静态资源请求则缓存到本地并返回。FastCGI 进程管理器采用固定静态(或动态)的子进程来处理请求,占用内存少,具有更好的扩展能力。

在CentOs 6.4上安装Nginx和PHP-FPM:

yum search nginx
sudo yum install nginx
sudo chkconfig --levels 235 nginx on
sudo service nginx start

yum search fpm
sudo yum install php54-fpm

编辑php.ini

sudo vim /etc/php.ini
; cgi.fix_pathinfo provides *real* PATH_INFO/PATH_TRANSLATED support for CGI.  PHP's
; previous behaviour was to set PATH_TRANSLATED to SCRIPT_FILENAME, and to not grok
; what PATH_INFO is.  For more information on PATH_INFO, see the cgi specs.  Setting
; this to 1 will cause PHP CGI to fix its paths to conform to the spec.  A setting
; of zero causes PHP to behave as before.  Default is 1.  You should fix your scripts
; to use SCRIPT_FILENAME rather than PATH_TRANSLATED.
; http://www.php.net/manual/en/ini.core.php#ini.cgi.fix-pathinfo
cgi.fix_pathinfo=0

启动PHP-FPM

chkconfig --levels 235 php-fpm on
service php-fpm start

更改Nginx worker_processes配置

sudo vim /etc/nginx/nginx.conf
worker_processes  4;

更改Nginx 配置以便处理php请求

sudo vim /etc/nginx/conf.d/default.conf
server {
    listen       80 default_server;
    server_name  _;

    #charset koi8-r;

    #access_log  logs/host.access.log  main;

    # Load configuration files for the default server block.
    include /etc/nginx/default.d/*.conf;

    location / {
        root   /usr/share/nginx/html;
        index  index.html index.htm;
    }

    error_page  404              /404.html;
    location = /404.html {
        root   /usr/share/nginx/html;
    }

    # redirect server error pages to the static page /50x.html
    #
    error_page   500 502 503 504  /50x.html;
    location = /50x.html {
        root   /usr/share/nginx/html;
    }

    # proxy the PHP scripts to Apache listening on 127.0.0.1:80
    #
    #location ~ \.php$ {
    #    proxy_pass   http://127.0.0.1;
    #}

    # pass the PHP scripts to FastCGI server listening on 127.0.0.1:9000
    #
    location ~ \.php$ {
        root           /usr/share/nginx/html;
        #fastcgi_pass   127.0.0.1:9000;
        fastcgi_pass   unix:/tmp/php5-fpm.sock;
        fastcgi_index  index.php;
        fastcgi_param  SCRIPT_FILENAME  /scripts$fastcgi_script_name;
        include        fastcgi.conf;
    }

    # deny access to .htaccess files, if Apache's document root
    # concurs with nginx's one
    #
    #location ~ /\.ht {
    #    deny  all;
    #}
}

编辑PHP-FPM

sudo vim /etc/php-fpm.d/www.conf
; List of ipv4 addresses of FastCGI clients which are allowed to connect.
; Equivalent to the FCGI_WEB_SERVER_ADDRS environment variable in the original
; PHP FCGI (5.2.2+). Makes sense only with a tcp listening socket. Each address
; must be separated by a comma. If this value is left blank, connections will be
; accepted from any ip address.
; Default Value: any
;listen.allowed_clients = 127.0.0.1
listen = /tmp/php5-fpm.sock

; Set permissions for unix socket, if one is used. In Linux, read/write
; permissions must be set in order to allow connections from a web server. Many
; BSD-derived systems allow connections regardless of permissions.
; Default Values: user and group are set as the running user
;                 mode is set to 0660
listen.owner = nginx
listen.group = nginx
listen.mode = 0660

; Unix user/group of processes
; Note: The user is mandatory. If the group is not set, the default user's group
;	will be used.
; RPM: apache Choosed to be able to access some dir as httpd
user = nginx
; RPM: Keep a group allowed to write in log dir.
group = nginx

重启PHP-FPM

sudo service php-fpm restart

创建PHP页面

sudo vim /usr/share/nginx/html/info.php
<?php
phpinfo();

重启Nginx

sudo service nginx restart

测试PHP是否正确处理。
php-fpm

参考链接:
Nginx and PHP-FPM Configuration and Optimizing Tips and Tricks
nginx
FastCGI 进程管理器(FPM)
Apache + mod_php compared to Nginx + php-fpm
Which PHP mode? Apache vs CGI vs FastCGI
PHP: What are the advantages of FastCGI over mod_php?
What are the advantages of using PHP-FPM + Nginx over Apache and mod_php?
How To Install Nginx With PHP5 (And PHP-FPM) And MySQL Support On CentOS 6.5
How To Install Linux, nginx, MySQL, PHP (LEMP) stack on CentOS 6
Linux上配置Nginx+PHP5(FastCGI)
使用fastcgi_finish_request提高页面响应速度
善用php-fpm的慢执行日志slow log,分析php性能问题
swoole之代码热更新实现