关于HHVM/Hack/Jit和PHP的性能问题

2014年4月18日 没有评论

最近HHVM/Hack很火,群里有不少人已经开始预言HHVM将会取代Zend。

HHVM为什么比Zend PHP快?

读过PHP内核实现文章的人都懂得PHP的zend vm是一个很高级的虚拟机,zend先将PHP代码进行编译生成一种二进制指令,称之为opcode。然后再用zend vm逐条执行。而PHP的用户函数、运行时局部变量,全局变量,常量都是放在一个Hashtable中。每一条opcode指令都对应一个C函数。

执行一次C函数的开销主要是1:参数的入栈出栈,2:CPU寄存器状态保存。比如在PHP里执行1千万次加法。

$j = 0;
for ($i = 0; $i < 10000000; $i++) {
    $j += $i;
}

这里会发生1000万次函数调用。但如果是编译为机器码就快得多了。一个2.0GHz的CPU,理论上每秒可以执行20亿次指令。如果换成函数调用,1秒差不多只能运行一千万次。所以编译为机器码执行的语言,如C/C++,Golang或者拥有JIT的语言,如Java,Node.js(V8),LuaJIT,HHVM单从指令执行的角度上来看至少比PHP快几十倍。

对于字符串处理,JSON解码编码,iconv编码转换,数组操作等,PHP比C++,Java慢,这是不正确的。在PHP里这类操作都是由C扩展函数完成的,性能与编译型语言一致。所以真正慢的只是PHP的写的代码,如用户函数,类和对象操作等。

运算密集型/IO密集型

运算密集型程序,程序内需要大量执行内存复制操作、循环、运算指令等。此类程序的瓶颈就是CPU,增加性能的方法就是提升CPU硬件配置,改进算法,提升语言/工具的执行性能。对于此类程序,PHP的性能问题就会凸显出来。执行相同的逻辑,PHP要比C/C++慢几十倍甚至上百倍,这是不可接受的。

IO密集型程序,程序内主要的瓶颈是IO等待,比如我们有一个Http请求要执行100ms才返回结果,其中有90ms在查询数据库,8ms的时间在读写文件。那无论使用C/C++还是PHP,请求响应时间总是100ms左右。语言性能优化只有这2ms的空间。

我们常见的程序里大部分都是IO密集型程序,很少有运算密集的,语言本身的性能差异可以忽略。遇到需要密集计算的模块,也可以使用C/C++来实现,并提供PHP扩展。

像HHVM这样提升PHP性能的工具,在超大型PHP集群确实是有价值的,提升性能后可以节约一部分机器成本。

 

分类: Linux, PHP, PHP系统编程 标签:

PHP开发异步高性能的MySQL代理服务器

2014年3月6日 2 条评论

MySQL数据库对每个客户端连接都会分配一个线程,所以连接非常宝贵。开发一个异步的MySQL代理服务器,PHP应用服务器可以长连接到这台Server,既减轻MYSQL的连接压力,又使PHP保持长连接减少connect/close的网络开销。

此Server考虑到了设置了数据库连接池尺寸,区分忙闲,mysqli断线重连,并设置了负载保护。基于swoole扩展开发,io循环使用epoll,是全异步非阻塞的,可以应对大量TCP连接。

程序的逻辑是:启动时创建N个MySQL连接,收到客户端发来的SQL后,分配1个MySQL连接,将SQL发往数据库服务器。然后等待数据库返回查询结果。当数据库返回结果后,再发给对应的客户端连接。

核心的数据结构是3个PHP数组。idle_pool是空闲的数据库连接,当有SQL请求时从idle_pool中移到busy_pool中。当数据库返回结果后从busy_pool中再移到idle_pool中,以供新的请求使用。当SQL请求到达时如果没有空闲的数据库连接,那会自动加入到wait_queue中。一旦有SQL完成操作,将自动从wait_queue中取出等待的请求进行处理。

如此循环使用。由于整个服务器是异步的单进程单线程所以完全不需要锁。而且是完全异步的,效率非常高。

当然本文的代码,如果要用于生产环境,还需做更多的保护机制和压力测试。在此仅抛砖引玉,提供一个解决问题的思路。


class DBServer
{
    protected $pool_size = 20;
    protected $idle_pool = array(); //空闲连接
    protected $busy_pool = array(); //工作连接
    protected $wait_queue = array(); //等待的请求
    protected $wait_queue_max = 100; //等待队列的最大长度,超过后将拒绝新的请求

    /**
     * @var swoole_server
     */
    protected $serv;

    function run()
    {
        $serv = new swoole_server("127.0.0.1", 9509);
        $serv->set(array(
            'worker_num' => 1,
        ));

        $serv->on('WorkerStart', array($this, 'onStart'));
        //$serv->on('Connect', array($this, 'onConnect'));
        $serv->on('Receive', array($this, 'onReceive'));
        //$serv->on('Close', array($this, 'onClose'));
        $serv->start();
    }

    function onStart($serv)
    {
        $this->serv = $serv;
        for ($i = 0; $i < $this->pool_size; $i++) {
            $db = new mysqli;
            $db->connect('127.0.0.1', 'root', 'root', 'test');
            $db_sock = swoole_get_mysqli_sock($db);
            swoole_event_add($db_sock, array($this, 'onSQLReady'));
            $this->idle_pool[] = array(
                'mysqli' => $db,
                'db_sock' => $db_sock,
                'fd' => 0,
            );
        }
        echo "Server: start.Swoole version is [" . SWOOLE_VERSION . "]\n";
    }

    function onSQLReady($db_sock)
    {
        $db_res = $this->busy_pool[$db_sock];
        $mysqli = $db_res['mysqli'];
        $fd = $db_res['fd'];

        echo __METHOD__ . ": client_sock=$fd|db_sock=$db_sock\n";

        if ($result = $mysqli->reap_async_query()) {
            $ret = var_export($result->fetch_all(MYSQLI_ASSOC), true) . "\n";
            $this->serv->send($fd, $ret);
            if (is_object($result)) {
                mysqli_free_result($result);
            }
        } else {
            $this->serv->send($fd, sprintf("MySQLi Error: %s\n", mysqli_error($mysqli)));
        }
        //release mysqli object
        $this->idle_pool[] = $db_res;
        unset($this->busy_pool[$db_sock]);

        //这里可以取出一个等待请求
        if (count($this->wait_queue) > 0) {
            $idle_n = count($this->idle_pool);
            for ($i = 0; $i < $idle_n; $i++) {
                $req = array_shift($this->wait_queue);
                $this->doQuery($req['fd'], $req['sql']);
            }
        }
    }

    function onReceive($serv, $fd, $from_id, $data)
    {
        //没有空闲的数据库连接
        if (count($this->idle_pool) == 0) {
            //等待队列未满
            if (count($this->wait_queue) < $this->wait_queue_max) {
                $this->wait_queue[] = array(
                    'fd' => $fd,
                    'sql' => $data,
                );
            } else {
                $this->serv->send($fd, "request too many, Please try again later.");
            }
        } else {
            $this->doQuery($fd, $data);
        }
    }

    function doQuery($fd, $sql)
    {
        //从空闲池中移除
        $db = array_pop($this->idle_pool);
        /**
         * @var mysqli
         */
        $mysqli = $db['mysqli'];

        for ($i = 0; $i < 2; $i++) {
            $result = $mysqli->query($sql, MYSQLI_ASYNC);
            if ($result === false) {
                if ($mysqli->errno == 2013 or $mysqli->errno == 2006) {
                    $mysqli->close();
                    $r = $mysqli->connect();
                    if ($r === true) continue;
                }
            }
            break;
        }

        $db['fd'] = $fd;
        //加入工作池中
        $this->busy_pool[$db['db_sock']] = $db;
    }
}

$server = new DBServer();
$server->run();
分类: Linux, PHP, Swoole扩展 标签:

异步AIO的研究

2014年3月5日 没有评论

首先声明一下epoll+nonblock从宏观角度可以叫做全异步,但从微观的角度来看还是同步的IO。只是在数据到达后得到系统通知,然后同步执行recv取回数据,没有iowait。

真正的异步IO(下面会统一叫做AIO)应该像Windows IOCP一样,传入文件句柄,缓存区,尺寸等参数和一个函数指针,当操作系统真正完成了IO操作,再执行对应的函数。

实际上对于socket来说,epoll已经是最高效的模型了,虽然比AIO多一次recv系统调用,但总体来看没有任何IO等待,效率很高。而且epoll是天然的reactor模型,程序实现更容易。AIO如windows的IOCP,是异步回调的方式,开发难度很高。

为什么还是需要AIO呢,原因是文件句柄跟socket完全不同,它总是处于可读状态。不能使用epoll+nonblock来实现异步化。如果在一个epoll的全异步Server中,要读写文件那必须得使用AIO。下面说下AIO的几种实现方案。

gcc AIO

gcc遵循posix标准实现了AIO。头文件为 <aio.h>,支持FreeBSD/Linux。是通过阻塞IO+线程池来实现的。主要的几个函数是aio_read/aio_write/aio_return。

优点:支持平台多,兼容性好,无需依赖第三方库,阻塞IO可以利用到操作系统的PageCache。

缺点:据说有一些bug和陷阱,一直未解决。不过这个都是网上文章中讲的,gcc发展这么多年,不至于还有遗留bug吧。这里有待测试。

Linux Native Aio

由操作系统内核提供的AIO,头文件为<linux/aio_abi.h>。Native Aio是真正的AIO,完全非阻塞异步的,而不是用阻塞IO和线程池模拟。主要的几个系统调用为io_submit/io_setup/io_getevents。

优点:由操作系统提供,读写操作可以直接投递到硬件,不会浪费CPU。

缺点:仅支持Linux,必须使用DirectIO,所以无法利用到操作系统的PageCache。对于写文件来说native aio的作用不大,应为本身写文件就是先写到PageCache上,直接返回,没有IO等待。

Libeio

libev的作者开发的AIO实现,与gcc aio类似也是使用阻塞IO+线程池实现的。优点与缺点参见上面。它与gcc aio的不同之处,代码更简洁,所以bug少更安全稳定。但这是一个第三方库,你的代码需要依赖libeio。

总结

如果你的程序读写的文件很大,随即性强,这样PageCache的命中率低,那可以选择Native AIO,降低CPU使用率。

如果读写的文件很小,而且是固定的一些文件,这样PageCache的命中率高,可以选择gcc aio或者libeio。

 

 

 

分类: C/C++, Linux 标签:

swoole的异步和同步

2014年2月1日 没有评论

有人问node.js和swoole有何不同,最大的不同当然是语言了,一个是Javascript一个是PHP。除此之外最大的不同是,swoole不仅支持异步,还支持同步。而node.js只支持异步,代码中不能有任何阻塞,否则程序就会变得效率很差。

swoole如果选择异步模式,所有网络连接应当使用swoole_client。其他数据库查询,API调用都应当使用swoole_event_add将socket加入到异步事件循环中。一些必须要阻塞的操作就用swoole提供的task/finish功能进行异步化。异步方式单个worker进程的程序效率很高,不存在IOWait,所以一个进程就能同时处理成千上万的请求,只需要开启CPU核数的1倍或2倍的worker进程即可。

如果选择阻塞模式,那在你的代码中就像php-fpm或mod_php中那样编码即可。另外根据系统的负载情况设定足够多的worker进程。比如每个请求需要50ms,那单个worker进程只能达到20QPS。启动100个worker进程就可以有2000QPS的处理能力。当然进程越多那浪费在进程间切换的CPU时间就会越多,系统的效率越来越差。

有人还提出假如Server的处理能力是100QPS,那同时并发10000个请求怎么办?这个问题就像银行窗口一样,假如他们的处理能力是每小时100个客户,假设来了1万个人,那只能排队等候处理或者丢弃一部分人。Server也是一样,超过处理能力的请求都会排队放在socket缓冲区中,如果socket缓冲区也满了,那新的请求就将丢弃。

分类: PHP, Swoole扩展 标签:

基于swoole扩展实现真正的PHP数据库连接池

2014年1月15日 2 条评论

PHP的数据库连接池一直以来都是一个难题,很多从PHP语言转向Java的项目,大多数原因都是因为Java有更好的连接池实现。PHP的MySQL扩展提供了长连接的API,但在PHP机器数量较多,规模较大的情况下,mysql_pconnect非但不能节约MySQL资源,反而会加剧数据库的负荷。

假设有100台PHP的应用服务器,每个机器需要启动100个apache或fpm工作进程,那每个进程都会产生一个长连接到MySQL。这一共会产生1万个My SQL连接。大家都知道MySQL是每个连接会占用1个线程。那MYSQL就需要创建1万个线程,这样大量的系统资源被浪费在线程间上下文切换上。而你的业务代码中并不是所有地方都在做数据库操作,所以这个就是浪费的。

连接池就不同了,100个worker进程,公用10个数据库连接即可,当操作完数据库后,立即释放资源给其他worker进程。这样就算有100台PHP的服务器,那也只会创建1000个MySQL的连接,完全可以接受的。

以前确实没有好的办法来解决此问题的,现在有了swoole扩展,利用swoole提供的task功能可以很方便做出一个连接池来。

代码如下:


$serv = new swoole_server("127.0.0.1", 9508);
$serv->set(array(
    'worker_num' => 100,
    'task_worker_num' => 10, //MySQL连接的数量
));

function my_onReceive($serv, $fd, $from_id, $data)
{
    //taskwait就是投递一条任务,这里直接传递SQL语句了
    //然后阻塞等待SQL完成
    $result = $serv->taskwait("show tables");
    if ($result !== false) {
        list($status, $db_res) = explode(':', $result, 2);
        if ($status == 'OK') {
            //数据库操作成功了,执行业务逻辑代码,这里就自动释放掉MySQL连接的占用
            $serv->send($fd, var_export(unserialize($db_res), true) . "\n");
        } else {
            $serv->send($fd, $db_res);
        }
        return;
    } else {
        $serv->send($fd, "Error. Task timeout\n");
    }
}

function my_onTask($serv, $task_id, $from_id, $sql)
{
    static $link = null;
    if ($link == null) {
        $link = mysqli_connect("127.0.0.1", "root", "root", "test");
        if (!$link) {
            $link = null;
            $serv->finish("ER:" . mysqli_error($link));
            return;
        }
    }
    $result = $link->query($sql);
    if (!$result) {
        $serv->finish("ER:" . mysqli_error($link));
        return;
    }
    $data = $result->fetch_all(MYSQLI_ASSOC);
    $serv->finish("OK:" . serialize($data));
}

function my_onFinish($serv, $data)
{
    echo "AsyncTask Finish:Connect.PID=" . posix_getpid() . PHP_EOL;
}

$serv->on('Receive', 'my_onReceive');
$serv->on('Task', 'my_onTask');
$serv->on('Finish', 'my_onFinish');
$serv->start();

这里task_worker_num就是要启用的数据库连接池数量,worker进程为100时,连接池数量为10就可以设置为worker_num = 100, task_worker_num = 10。

Yii/Yaf/Swoole3个框架的压测性能对比

2013年12月18日 1 条评论

本次压测使用的是一台4核I5 CPU,8G内存的笔记本。使用的版本是:

swoole_framework github最新版
Yaf 2.2.9
Yii-1.1.14

php-fpm和swoole应用服务器均启用了8个进程,因为仅比较hello world,不存在阻塞,8进程足以压满CPU。php5-fpm中已经启用了apc对所有PHP进行缓存。

压测使用ab工具,参数为:

 ab -c 100 -n 10000

Yaf

Server Software: nginx/1.2.6
 Server Hostname: localhost
 Server Port: 80
 Document Path: /yafapp/
 Document Length: 11 bytes
 Concurrency Level: 100
 Time taken for tests: 1.336 seconds
 Complete requests: 10000
 Failed requests: 0
 Write errors: 0
 Total transferred: 1770000 bytes
 HTML transferred: 110000 bytes
 Requests per second: 7486.03 [#/sec] (mean)
 Time per request: 13.358 [ms] (mean)
 Time per request: 0.134 [ms] (mean, across all concurrent requests)
 Transfer rate: 1293.97 [Kbytes/sec] received

Yii

Server Software: nginx/1.2.6
 Server Hostname: localhost
 Server Port: 80
 Document Path: /yiiapp/
 Document Length: 11 bytes
 Concurrency Level: 100
 Time taken for tests: 8.016 seconds
 Complete requests: 10000
 Failed requests: 0
 Write errors: 0
 Total transferred: 1770000 bytes
 HTML transferred: 110000 bytes
 Requests per second: 1247.55 [#/sec] (mean)

SwooleFramework

 Server Software: Swoole
 Server Hostname: 127.0.0.1
 Server Port: 8888
 Document Path: /hello/index
 Document Length: 11 bytes 
 Concurrency Level: 100
 Time taken for tests: 1.470 seconds
 Complete requests: 10000
 Failed requests: 0
 Write errors: 0
 Total transferred: 2750000 bytes
 HTML transferred: 110000 bytes
 Requests per second: 6801.78 [#/sec] (mean)
 Time per request: 14.702 [ms] (mean)
 Time per request: 0.147 [ms] (mean, across all concurrent requests)
 Transfer rate: 1826.65 [Kbytes/sec] receive

结果评价

有些人说框架的消耗和业务逻辑相比不算什么,这句话在你的QPS不到10个时是完全正确的。但如果你的网站访问量很大,机器负载很高,那么框架的性能消耗就会很明显的体现出来了。

Yii压测只有1200QPS,假设网站的平均QPS为200,因为框架的消耗是纯CPU消耗,也就是光Yii就会带来(200/1200)*100%=16.7%的CPU消耗。而Yaf/Swoole的QPS可达7000,200qps仅占用

(200/7000)*100%=2.8%的CPU消耗。当然200QPS对于一个巨型网站来说只是一个小数目,框架的CPU消耗会随着QPS增加而直线上升。

Swoole框架的早期版本可达9000QPS,swoole框架本身还是很复杂的,因为全部是PHP的代码实现,仅底层使用了swoole扩展,如果做了足够的精简和优化,还是有很大的上升空间。

压测证明Yaf的性能非常好。如果你希望使用PHP应用服务器可以选择Swoole,如果是LAMP/LNMP架构可以使用Yaf框架。

分类: PHP, Swoole扩展, Swoole框架 标签:

swoole的worker进程之间如何共享数据

2013年12月9日 2 条评论

swoole的TCP连接都是以数字的方式提供给PHP端的,在PHP代码中只需要保存fd/from_id这2个数字,即可向对应的连接发送数据。swoole本身也提供了可以遍历所有连接的函数接口(swoole_connection_list/swoole_connection_info)。这两个函数在EventWorker/TaskWorker均可调用。
但只能保存与网络服务相关的数据,如来源端口,客户端IP,客户端port等信息。swoole本身没有提供操作共享内存的接口,如果业务代码中希望保存额外的信息,或在worker进程之间共享数据,可以使用第三方的工具库。

单机环境单应用

可以使用apc扩展提供的apc_store/apc_fetch/apc_delete来保存数据,使用APCIterator来遍历数据,APCIterator可以使用正则来匹配key,功能还是很强大的。
apc是使用锁+共享内存实现的,性能很好。但只能用于单个Swoole的服务器实例,并且数据是非持久化的,Server结束时数据会被清空。如果想要保存APC中的数据可以使用 apc_bin_dumpfile 和 apc_bin_loadfile 。


if(pcntl_fork() > 0)
{
    apc_store("hello", "swoole");
    sleep(10000);
}
else
{
    usleep(1000);
    echo "Child:",apc_fetch("hello2");
}

读写内存文件

Linux/FreeBSD下都提供了tmpfs,基于内存的虚拟文件系统。Linux下是/dev/shm,读写这个目录下的文件没有磁盘IO,只是内存操作,性能非常高。Oracle数据库中就用到了它。

Swoole的应用中可以使用/dev/shm + swoole_lock来存储/共享运行时的数据。

单机跨应用

levelDB/rocksdb或者读写文件。文件读写时应当加锁,可以使用swoole提供的swoole_lock类。当然也可以直接使用分布式的存储,如memcache/mysql/redis等。

分布式环境

使用memcache/mysql/redis/ttserver等存储服务。

 

分类: Swoole扩展 标签:

为什么选择swoole?

2013年11月25日 没有评论

Golang

golang很不错,性能好,开发效率高。协程+通道既解决了异步IO,又解决了并行问题。但Go目前存在gc和协程调度的问题,Java之类语言花了近20年的时间来完善GC,GO估计3-5年之内很难解决。同样node.js也存在此问题。

gc问题:golang和node.js都是定期进行全局gc,当发生gc时,所有代码都停止运行。如果内存申请释放很复杂,需要很长的时间才能处理完毕。程序会发生周期性卡顿。
协程调度:golang是编译为机器码执行的,如果某个协程占用的cpu时间过长,其他协程会得不到执行。这和erlang不同,erlang是运行在VM之上的,VM可以自己进行调度。机器码调度只能靠CPU中断,而且一般只有操作系统有调度的权力。Golang中只能依赖手工在代码中加入Yield(),转让CPU时间片。

Node.js

单进程单线程,全异步,性能很好。但应用层不能有阻塞,需要写大量嵌套回调才能实现业务逻辑。开发者心智负担太高。随着业务代码增加,开发和维护越来越困难。

Erlang

erlang也是协程+通道,和golang相同。Erlang确实是网络编程的终极方案,而且erlang是基于VM执行的,不存在golang的2个问题。erlang是函数式编程,比较难入门,开发效率不高。

php+libevent

libevent只是一个事件轮询的库,epoll的封装。libevent过于底层,不是整套方案,你需要自行写大量底层代码。维护成本很高,另外一旦出了问题,你需要非常熟悉libevent才能解决。

Swoole

swoole与node.js/react之类不同的是,swoole是整体方案。从性能、异步、并行、可维护性、开发效率方面综合考虑的。swoole中你可以同步,也可以异步。代码中可以有阻塞,通过多开进程来调整处理能力。

既保证性能,又保证开发效率。

分类: Swoole扩展 标签:

Java是严谨的编程语言吗?

2013年11月12日 1 条评论

未必。就拿对象属性来说。在java里是直接写名称来用,直接xxx,这点和C++一样。而PHP是通过 this.xxx来调用。

Java代码:


public class Test
{
   private int xxx;
   public static void main(String[] args)
   {
       xxx = 12345;
   }
}

PHP代码:


class Test
{
    private $xxx;
    function main()
    {
        $this->xxx = 12345;
    }
}

Java/C++这样的语法,显然不好区分哪个是局部变量,哪个是对象属性。而PHP就一目了然了。

分类: C/C++, Java 标签:

PHP实现真正的异步MySQL

2013年11月7日 1 条评论

node.js之类的语言可以实现异步的数据库查询功能,执行SQL语句之后不必等待数据库返回结果。继续去执行其他的代码,当数据库返回结果是再对数据进行处理,如渲染页面,并将HTML页面发送给客户端。这样应用程序完全不需要阻塞等待。这种方式运行效率非常高。

PHP中是否可以实现类似的异步非阻塞MySQL查询呢?使用github搜索发现一个项目貌似是做此功能的,https://github.com/kaja47/async-mysql,查看代码是基于React.PHP.的。但并不是真正的异步SQL。实现的原理是设置一个定时器,每0.02秒轮询一次。虽然也可以用,但这样很浪费CPU资源。不是真正的异步MYSQL。

现在Swoole1.6.2提供了swoole_event_add和swoole_get_mysqli_sock 2个新的函数,使用它完全可以实现真正的PHP异步MySQL。下面讲一下具体的实现。

代码:


$db = new mysqli;
$db->connect('127.0.0.1', 'root', 'root', 'test');
$db->query("show tables", MYSQLI_ASYNC);
swoole_event_add(swoole_get_mysqli_sock($db), function($db_sock) {
global $db;
$res = $db->reap_async_query();
var_dump($res->fetch_all(MYSQLI_ASSOC));
swoole_event_exit();
});

首先连接mysql,连接成功后执行SQL语句,要在第二个参数指定MYSQLI_ASYNC。表示此查询为异步。query函数会立即返回,这时候并没有得到mysqli_result。

然后调用swoole_get_mysqli_sock函数取到mysql连接的socket,并调用swoole_event_add将它加入到swoole的epoll事件循环中。当数据库返回结果是会回调我们刚才制定的函数。

这时候再调用mysqli_reap_async_query得到result,调用fetch_all函数得到数据。

这个过程是完全异步非阻塞的,不存在任何浪费CPU的代码。这个代码还可以用在服务器端程序上,具体代码实现后续会再写一篇文章详细介绍。

 

分类: PHP, Swoole扩展 标签: