http://rango.swoole.com/archives/category/php_swoole


‘Swoole扩展’ 分类的存档


关于C++、PHP和Swoole



2015年8月25日 韩 天峰 评论已被关闭



昨天和一个前同事聊天,各种吐槽PHP,吐槽Swoole,他认为PHP到处是坑,PHP局限很大。PHP+Swoole不适合做高并发服务器,C+Swoole才是最好的方案。C++有各种数据结构,C++可以开线程,C++可以共享对象。看来有必要好好得说明一下了。

PHP比C/C++或Java少了什么?多线程,多线程,多线程……

是的。PHP比C/C++、Java少了多了多线程。PHP只有多进程的方案,所以PHP里的全局变量和对象不是共享的、数据结构也不能跨进程操作、Socket文件描述符不能共享等等。所以PHP有局限?

多线程看似比多进程要强大很多,实际上我可以负责任的告诉你,多线程带来的坑更多

  • 数据同步问题会让你崩溃的。要么就牺牲性能到处加锁,要么就用地狱难度的无锁并发编程,据我所知目前国内能掌握此项技能的人凤毛麟角。
  • 不要以为加锁就万事大吉了,你会在死锁问题上栽个大跟头。当你的程序逻辑复杂后,锁越来越难控制了,一旦死锁你的程序基本上就完了。
  • 某个线程挂了那所有线程都会退出

反而在看多进程,其实就简单的多了。



  • 配合进程间通信,基本上你可以实现任意的数据共享。比如利用一个进程专门存数据结构和对象,其他进程的数据操作全部投递到此进程来
  • 多进程不需要锁
  • 多进程可以使用共享内存的数据结构实现一些多线程的功能。如Swoole提供的Table、Atomic可以实现数据共享,但成本很低。未来还会加入共享内存队列

所谓PHP限制了Swoole,这完全是无稽之谈。合理利用Swoole提供的Table、Atomic、SendMessage/PipeMessage、Task完全可以实现异步非阻塞的代码逻辑。

C++写出来的程序性能更好?

这完全是盲目的迷信,密集计算的程序C++确实是有优势的。而并发服务器核心是IO,并非大规模密集运算。C++从语言层面来看并没有什么优势。另外C++中的大部分数据结构在PHP中都有对应的实现,实在不行自己写个专门的扩展也能解决之。

高并发的服务器单机能维持10W连接、每秒可处理3-5W笔消息收发。这种性能水准已经可以应用在BAT的核心系统上了。

开发效率快的意义是什么?

这位同事还说PHP开发Server虽然比C++快了,但是追求性能的极致还是要用C++。我要告诉你效率高了究竟意义何在。开发一套好程序不是一件容易的事情,需要程序员投入大量时间和精力。开发效率提升的意义并不是简单的我可以更少时间完工,而是剩下的时间你可以增加单元测试、修复BUG、提升用户体验、完善细节、提供配套工具、优化性能、增加关键日志、增加监控报警、增加容灾方案。

 




分类: C/C++PHPSwoole扩展 标签:


关于TCP网络通信



2015年8月10日 韩 天峰 评论已被关闭



TCP协议在底层机制上解决了UDP协议的顺序和丢包重传问题。但相比UDP又带来了新的问题,TCP协议是流式的,数据包没有边界。应用程序使用TCP通信就会面临这些难题。一些程序在本机测试是正确的,上线后就出现各种奇怪的BUG。如下面的伪代码,客户端向服务器端发送一个json字符串,服务器端接收此字符串。在慢速网络中Server无法正确接收完整的JSON字符串。

$client->send(json_encode('a' => $data_10k, 'b' => $data_5k));
$pkg = $server->recv(); //Server收到的数据只有一小部分

$client->send("hello1");
$client->send("hello2");
$client->send("hello3");
$pkg = $server->recv(); //Server会一次性收到3个数据包

因为TCP通信是流式的,在接收1个大数据包时,可能会被拆分成多个数据包发送。多次Send底层也可能会合并成一次进行发送。这里就需要2个操作来解决:

  • 分包:Server收到了多个数据包,需要拆分数据包
  • 合包:Server收到的数据只是包的一部分,需要缓存数据,合并成完整的包

具体编码实现这里就不讲了,这是一个比较复杂的编程过程,稍有不慎就会出现严重的BUG

Swoole如何解决此问题

在swoole中可以使用Length/EOF 2种协议解析方式来完美解决此问题。从1.7.18版本开始,Swoole的Server/Client都支持了Length/EOF的协议处理方式,应用层代码只需要配置一下参数,就无需关注底层分包合包了。每次onReceive收到的数据包总是完整的。



Server/Client的配置是相同的



EOF

$server->set(array('open_eof_split' => true, 'package_eof' => "\r\n"));

EOF协议处理的原理是每个数据包结尾加一串特殊字符表示包已结束。如memcache、ftp、stmp都使用\r\n作为结束符。发送数据时只需要在包末尾增加\r\n即可。使用EOF协议处理,一定要确保数据包中间不会出现EOF,否则会造成分包错误。

Length

$server->set(array(
'open_length_check' => true,
'package_max_length' => 81920,
'package_length_type' => 'n', //see php pack()
'package_length_offset' => 0,
'package_body_offset' => 2,
));

固定包头的协议非常通用,在BAT的服务器程序中经常能看到。这种协议的特点是一个数据包总是由包头+包体2部分组成。包头由一个字段指定了包体或整个包的长度,长度一般是使用2字节/4字节整数来表示。服务器收到包头后,可以根据长度值来精确控制需要再接收多少数据就时完整的数据包。Swoole的配置可以很好的支持这种协议,可以灵活地设置4项参数应对所有情况。

Swoole的Server和异步Client都是在onReceive回调函数中处理数据包,当设置了协议处理后,只有收到一个完整数据包时才会触发onReceive事件。同步客户端在设置了协议处理后,调用 $client->recv() 不再需要传入长度,recv函数在收到完整数据包或发生错误后返回。

 




分类: PHPSwoole扩展 标签:


swoole新增的tick定时器



2015年3月24日 韩 天峰 评论已被关闭



swoole-1.7.14增加了tick定时器,解决了addtimer存在的各种问题。

swoole_timer_tick(1000, function ($timer_id) {
    echo "tick-1000ms\n";
    swoole_timer_clear($timer_id);
})

swoole_timer_tick(2000, function () {
    echo "tick-2000ms\n";
})

swoole_timer_tick和swoole_timer_after原理是相同的,区别是after只执行一次就会销毁。tick定时器会持续执行,直到调用了swoole_timer_clear清除定时器。

与PHP代码中的pcntl_alarm不同,swoole的定时器是基于EventLoop实现的,不依赖系统信号和PHP的tick机制,所以效率非常高。即使添加大量定时器,CPU消耗也是很低的。PHP在过去没有像JavaScript这样方便的定时器功能,现在有swoole就可以很好地解决。

在网络游戏程序中经常要用定时器,PHP+swoole将会成为最佳的技术方案。




分类: Swoole扩展 标签:


PHP官方的pcntl_signal性能极差



2014年10月18日 韩 天峰 没有评论



很多纯PHP开发的后端框架中都使用了pcntl扩展提供的信号处理函数pcntl_signal,实际上这个函数的性能是很差的。首先看一段示例代码:

declare(ticks = 1);
pcntl_signal(SIGINT, 'signalHandler');

这段代码在执行pcntl_signal前,先加入了declare(ticks = 1)。因为PHP的函数无法直接注册到操作系统信号设置中,所以pcntl信号需要依赖tick机制。通过查看pcntl.c的源码实现发现。pcntl_signal的实现原理是,触发信号后先将信号加入一个队列中。然后在PHP的ticks回调函数中不断检查是否有信号,如果有信号就执行PHP中指定的回调函数,如果没有则跳出函数。

PHP_MINIT_FUNCTION(pcntl)
{
	php_register_signal_constants(INIT_FUNC_ARGS_PASSTHRU);
	php_pcntl_register_errno_constants(INIT_FUNC_ARGS_PASSTHRU);
	php_add_tick_function(pcntl_signal_dispatch TSRMLS_CC);

	return SUCCESS;
}

pcntl_signal_dispatch 函数的实现:

void pcntl_signal_dispatch()
{
	//.... 这里略去一部分代码,queue即是信号队列
	while (queue) {
		if ((handle = zend_hash_index_find(&PCNTL_G(php_signal_table), queue->signo)) != NULL) {
			ZVAL_NULL(&retval);
			ZVAL_LONG(¶m, queue->signo);

			/* Call php signal handler - Note that we do not report errors, and we ignore the return value */
			/* FIXME: this is probably broken when multiple signals are handled in this while loop (retval) */
			call_user_function(EG(function_table), NULL, handle, &retval, 1, ¶m TSRMLS_CC);
			zval_ptr_dtor(¶m);
			zval_ptr_dtor(&retval);
		}
		next = queue->next;
		queue->next = PCNTL_G(spares);
		PCNTL_G(spares) = queue;
		queue = next;
	}
}

这样就存在一个比较严重的性能问题,大家都知道PHP的ticks=1表示每执行1行PHP代码就回调此函数。实际上大部分时间都没有信号产生,但ticks的函数一直会执行。如果一个服务器程序1秒中接收1000次请求,平均每个请求要执行1000行PHP代码。那么PHP的pcntl_signal,就带来了额外的 1000 * 1000,也就是100万次空的函数调用。这样会浪费大量的CPU资源。

比较好的做法是去掉ticks,转而使用pcntl_signal_dispatch,在代码循环中自行处理信号。而swoole中因为底层是C实现的,信号处理不受PHP的影响。swoole使用了目前Linux系统中最先进的signalfd来处理信号,几乎是没有任何额外消耗的。




分类: PHP系统编程Swoole扩展进程间通信 标签:


swoole与phpdaemon/reactphp/workerman等纯PHP网络库的差异



2014年6月17日 韩 天峰 1 条评论



从swoole项目开始到现在,一直有人在问这个问题。今天来抽空讲一下它。为什么swoole非要使用纯C来写而不是PHP代码来实现,核心的原因有2点:

1. PHP无法直接调用操作系统API

如sendfile、eventfd、timerfd、pthread等等,这里就不一一列举了,所以纯PHP实现的phpdaemon,reactphp,还有最近刚刚出来的workerman。这些框架都是基于PHP的sockets/pcntl/stream/libevent扩展实现,提供的功能很有限,很多功能都无法实现。如

  • 多线程
  • 毫秒定时器(PHP中只有秒级的定时器)
  • 标准输入输出重定向
  • MySQL/CURL异步化
  • 守护进程化
  • sendfile
  • 其他更多

而C语言写的swoole可以直接调用操作系统底层API,没有局限,swoole可以实现任何功能特性。

2. PHP的内存管理粒度太粗

PHP中想要做内存管理太困难了,基本上只有Array可用。在高并发大负载的网络Server中,内存复制简直就是性能杀手。PHP中根本无法解决此问题。

举一个简单的例子,客户端向服务器发起一个800K的包,每次发送8K,共发送100次。Server也会分成100次收到数据。那么PHP中拼接此数据包的方法是 $package .= $recv_data 。共需要复制100次内存,第一次为 8K+ 8K,第二次是 16K + 8K,第三次24K + 8K,依次类推,仅仅一次请求就发生了大量的内存拷贝。如果每秒有10万次请求,这个Server的性能必然极差。

而纯C的代码可以做到0次内存拷贝,在请求到来申请一块800K的buffer内存,通过指针运算,直接将数据写入buffer。一气呵成,内存拷贝为0。

当然这里仅是其中一个小小的点,真正的代码中不止这些。通过压测也能发现,纯C的swoole写一个EchoServer,做-c 500 -n 100000的测试中,CPU始终在5%-10%之间。而PHP实现的PSF网络Server框架,CPU占用率高达70%-90%。

 

以上也就是swoole和其他网络框架的差异。除此之外swoole以扩展方式提供,免去了代码中include php文件的问题。不需要去包含一堆外部文件,更容易融合到现有代码中。使用者仅需掌握swoole扩展的API即可。reactphp提供了API封装,耦合程度较低。phpdaemon/workerman耦合太高,不是你的代码集成它们,而是它们的代码集成你的代码。而且还需要了解其内部结构和耦合关系。

再看swoole,它其实就像MySQL之类的扩展一样,仅仅是作为一层API存在,耦合度非常低。swoole一直坚持低耦合高内聚,API化。用户可以方便的将swoole的功能集成到自己的代码中。

 




分类: Swoole扩展 标签:


node.js与swoole相比优势与劣势分析



2014年5月7日 韩 天峰 没有评论



这几天微博上好多网友在询问node.js和swoole对比的问题。这里做一个详细的解答。

多核并行

node.js的event loop是单进程单线程的,只有一个epoll/kqueue事件轮询被执行。所以无法利用到多核的计算优势。

swoole的event loop是多线程的,是基于epoll/kqueue的Multi-Reactor模型。这点与nginx/golang相同。另外swoole的多线程Reactor之间不存在锁,这点与nginx不同。它启用了专门的accept线程,类似与JAVA的netty。

所以在多核CPU的机器上,node.js对网络IO事件的处理能力绝对是要差swoole数倍的,在4核的机器上至少要查2-3倍。

虽然node.js也提供了多线程的扩展,但对于event_loop来说必须是内核提供,扩展不行的。

另外node.js未来是否会改造成为多线程Reactor,我估计不会,这不是技术上的难题。而是一旦改成多线程Reactor,node.js恐怕就不是node.js了。失去了原来单线程的各种优势后,反而会一无是处。当然可能node.js官方开发组会思考出解决问题的巧妙办法,一切都是后话了。

不只是event_loop,执行用户层代码是也是单线程的,不能利用多核计算优势。需要用户自己去fork多进程或者创建线程池。使用难度增加了很多。不像swoole,配置一下参数即可,是天然多进程。

异步网络IO

node.js和swoole都是基于epoll/kqueue实现的全异步非阻塞IO,所以这方面大同小异,没有差别。维持TCP长连接的能力是一样的。

node.js在这里有一个优势就是它支持windows的IOCP。swoole仅支持Linux/FreeBSD/MacOS的epoll/kqueue.

异步文件读写

node.js和swoole都提供了基于线程池的异步文件读写,DNS查询功能。node.js最早基于libeio实现,后来才自行实现线程池。swoole一开始就基于线程池设计的。

实际上这里都是通过多线程阻塞来模拟的,并非真正的异步读写文件。比如同时读写数百个文件时,性能远不如普通的多进程PHP程序。

swoole中还提供了Linux Native AIO的支持,是真正的内核层异步并行文件读写,不过需要通过修改宏开关,重新编译才可以使用。

最后结论

node.js和swoole比没有明显优势,仅在Windows支持方面比swoole要好。node.js中有的特性swoole中都有。个人认为Node.js最大的优势在于:

  • node.js使用JavaScript语言,与浏览器、HTML之间的融合度非常高,使用同一种语言既写前端又写后端
  • 支持Windows平台,利用node-webkit,可以开发PC客户端软件

node.js的定位应该是前端与后端结合非常紧密的应用场景。如websocket推送,JSON-RPC,轻量级HTTP接口。

而对于真正专业的后端领域,分布式系统,node.js不适合。

 




分类: Node.jsPHPSwoole扩展 标签: Node.js


swoole的进程模型架构



2014年5月5日 韩 天峰 没有评论



C++、PHP和Swoole_node.js

swoole的强大之处就在与其进程模型的设计,既解决了异步问题,又解决了并行。

主线程MainReactor

swoole启动后主线程会负责监听server socket,如果有新的连接accept,主线程会评估每个Reactor线程的连接数量。将此连接分配给连接数最少的reactor线程。这样的好处是

  1. 每个reactor线程持有的连接数是非常均衡的,没有单个线程负载过高的问题
  2. 解决了惊群问题,尤其是拥有多个listen socket时,节约了线程唤醒和切换的开销

主线程内还接管了所有信号signal的处理,使Reactor线程运行中可以不被信号打断。



管理进程Manager

swoole运行中会创建一个单独的管理进程,所有的worker进程和task进程都是从管理进程Fork出来的。管理进程会监视所有子进程的退出事件,当worker进程发生致命错误或者运行生命周期结束时,管理进程会回收此进程,并创建新的进程。

管理进程还可以平滑地重启所有worker进程,以实现程序代码的重新加载。

异步Reactor线程

swoole拥有多线程Reactor,所以可以充分利用多核,开启CPU亲和设置后,Reactor线程还可以绑定单独的核,节约CPU Cache开销。

swoole的Reactor线程是全异步非阻塞的,即使你的worker进程用了同步模式,依然不影响reactor线程的性能。在worker进程组很繁忙的状况下,reactor线程完全不受影响,依然可以收发处理数据。

TCP是流式的,没有边界,所以处理起来很麻烦。Reactor线程可以根据EOF或者包头长度,自动缓存数据,组装数据包。等一个请求完全收到后,再投递给Worker进程。

同步或异步Worker进程

与传统的半同步半异步服务器不同,Swoole的worker进程可以是同步的也可以异步的,这样带来了最大的灵活性。当你的Server需要很高性能,业务逻辑较为简单时你可以选择异步模式。当业务逻辑复杂多变,可以选择同步模式。

这里要比Node.js强大太多了。

TaskWorker进程池

swoole除了Reactor线程,Worker进程外还提供了TaskWorker进程池,目的是为了解决在业务代码中,有些逻辑部分不需要马上执行。利用task进程池,可以方便的投递一个异步任务去执行,在Worker进程空闲时再去捕获任务执行的结果。

 




分类: Swoole扩展进程间通信 标签:


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();




分类: LinuxPHPSwoole扩展 标签:


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缓冲区也满了,那新的请求就将丢弃。




分类: PHPSwoole扩展 标签:


基于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。