存档

作者存档

关于C10K、异步回调、协程、同步阻塞

2014年10月31日 1 条评论

最近到处在争论这些话题,发现很多人对一些基础的常识并不了解,在此发表一文做一下解释。此文未必能解答所有问题,各位能有一个大致的了解就好。

C10K的由来

大家都知道互联网的基础就是网络通信,早期的互联网可以说是一个小群体的集合。互联网还不够普及,用户也不多。一台服务器同时在线100个用户估计在当时已经算是大型应用了。所以并不存在什么C10K的难题。互联网的爆发期应该是在www网站,浏览器,雅虎出现后。最早的互联网称之为Web1.0,互联网大部分的使用场景是下载一个Html页面,用户在浏览器中查看网页上的信息。这个时期也不存在C10K问题。

Web2.0时代到来后就不同了,1方面是普及率大大提高了,用户群体几何倍增长。2是互联网不再是单纯的浏览万维网网页,逐渐开始进行交互,而且应用程序的逻辑也变的更复杂,从简单的表单提交,到即时通信和在线实时互动。C10K的问题才体现出来了。每一个用户都必须与服务器保持TCP连接才能进行实时的数据交互。Facebook这样的网站同一时间的并发TCP连接可能会过亿。

腾讯QQ也是有C10K问题的,只不过他们是用了UDP这种原始的包交换协议来实现的,绕开了这个难题。当然过程肯定是痛苦的。如果当时有epoll技术,他们肯定会用TCP。后来的手机QQ,微信都采用TCP协议。

这时候问题就来了,最初的服务器都是基于进程/线程模型的,新到来一个TCP连接,就需要分配1个进程(或者线程)。而进程又是操作系统最昂贵的资源,一台机器无法创建很多进程。如果是C10K就要创建1万个进程,那么操作系统是无法承受的。如果是采用分布式系统,维持1亿用户在线需要10万台服务器,成本巨大,也只有Facebook,Google,雅虎才有财力购买如此多的服务器。这就是C10K问题的本质。

实际上当时也有异步模式,如:select/poll模型,这些技术都有一定的缺点,如selelct最大不能超过1024,poll没有限制,但每次收到数据需要遍历每一个连接查看哪个连接有数据请求。

Epoll异步非阻塞

既然有了C10K问题,程序员们就开始行动去解决它。于是FreeBSD推出了kqueue,Linux推出了epoll,Windows推出了IOCP。这些操作系统提供的功能就是为了解决C10K问题。因为Linux是互联网企业中使用率最高的操作系统,Epoll就成为C10K killer、高并发、高性能、异步非阻塞这些技术的代名词了。

epoll技术的编程模型就是异步非阻塞回调,也可以叫做Reactor,事件驱动,事件轮循(EventLoop)。Epoll就是为了解决C10K问题而生。使用Epoll技术,使得小公司也可以玩高并发。不需要购买很多服务器,有几台服务器就可以服务大量用户。Nginx,libevent,node.js这些就是Epoll时代的产物。

C100K,C1M,C10M,C100M …

C10K问题解决后,程序员又提出了更高的挑战,也就是最近在火热争论的C100K,C1M等。Epoll既然能解决C10K,解决什么C100K,C1M也是可以的。只不过这个已经没有意义了。一个公司有1亿用户难道他买不起1万台服务器嘛。WhatsApp有2亿用户,卖了150亿美元。1万台服务器最多花费5000万美元。

看到阿里技术保障部的人也在谈C10K话题,我要补充一下,搞路由器、交换机、网关、防火墙之类基础网络设备的人,就不要参与C10K话题了。我们说的是应用层程序。

协程,coroutine

当程序员还沉浸在解决C10K问题带来的成就感时,一个新的问题被抛出了。异步嵌套回调太TM难写了。尤其是Node.js层层回调,缩进了几十层,要把程序员逼疯了。于是一个新的技术被提出来了,那就是协程(coroutine)。这个技术本质上也是异步非阻塞技术,它是将事件回调进行了包装,让程序员看不到里面的事件循环。程序员就像写阻塞代码一样简单。比如调用 client->recv() 等待接收数据时,就像阻塞代码一样写。实际上是底层库在执行recv时悄悄保存了一个状态,比如代码行数,局部变量的值。然后就跳回到EventLoop中了。什么时候真的数据到来时,它再把刚才保存的代码行数,局部变量值取出来,又开始继续执行。

这个就像时间禁止的游戏一样,国王对巫师说“我必须马上得到宝物,不然就砍了你的脑袋”,巫师念了一句时间停止的咒语,直到过了1年后勇士们才把宝物送来。这时候巫师解开咒语,把宝物交给国王。这里国王就可以理解成协程,他根本没感觉到时间停止,在他停止到醒来期间发生了什么他不知道,也不关心。

这就是协程的本质。协程是异步非阻塞的另外一种展现形式。Golang,Erlang,Lua协程都是这个模型。

同步阻塞

再回到同步阻塞这个话题,不知道大家看完协程是否感觉得到,实际上协程和同步阻塞是一样的。答案是的。所以协程也叫做用户态进/用户态线程。区别就在于进程/线程是操作系统充当了EventLoop调度,而协程是自己用Epoll进行调度。

协程的优点是它比系统线程开销小,缺点是如果其中一个协程中有密集计算,其他的协程就不运行了。操作系统进程的缺点是开销大,优点是无论代码怎么写,所有进程都可以并发运行。

Erlang解决了协程密集计算的问题,它基于自行开发VM,并不执行机器码。即使存在密集计算的场景,VM发现某个协程执行时间过长,也可以进行中止切换。Golang由于是直接执行机器码的,所以无法解决此问题。所以Golang要求用户必须在密集计算的代码中,自行Yield。

实际上同步阻塞程序的性能并不差,它的效率很高,不会浪费资源。当进程发生阻塞后,操作系统会将它挂起,不会分配CPU。直到数据到达才会分配CPU。多进程只是开多了之后副作用太大,因为进程多了互相切换有开销。所以如果一个服务器程序只有1000左右的并发连接,同步阻塞模式是最好的。

异步回调和协程哪个性能好

协程虽然是用户态调度,实际上还是需要调度的,既然调度就会存在上下文切换。所以协程虽然比操作系统进程性能要好,但总还是有额外消耗的。而异步回调是没有切换开销的,它等同于顺序执行代码。所以异步回调程序的性能是要优于协程模型的。

这里是指Nginx这种多进程异步非阻塞程序。Node.js/Redis此类程序如果不开多个进程,由于无法利用多核计算优势,所以性能并不好。在Node.js中可以使用childprocess/cluster等扩展开启多进程以解决此问题。
分类: Linux, 互联网 标签:

关于未来世界与编程技术的畅想

2014年10月23日 没有评论

最近几年大量的资本投入到移动互联网,这个行业很是被看好。但懂行的人都知道这里泡沫太重,曾经有人以为微信商城一出手就可以压倒淘宝天猫,后来发现这个微信商城订单少的可怜。毫无疑问移动互联网被高估了。目前互联网市场,大家都可以看得到PC,游戏,Web,社交等市场基本上已经饱和了。而阿里巴巴、京东主导的电商确实一片欣欣向荣,市场前景非常好。国内目前电商仅仅在1,2线城市得到普及,3,4线城市以及小城镇电商还是处于一个试探的阶段,市场空间是极大的。所以前不久阿里会投入100亿去开拓地方小城镇市场。

个人认为移动互联网,云计算,以及最近火热的智能家居,车联网实际上都是为了物联网做铺垫,真正的未来科技是物联网。互联网只是颠覆了信息传递,而物联网可以颠覆人的生活。未来每一个电器设备都会有操作系统和无线网络芯片,比如冰箱、电视、空调、电灯、洗衣机等等。设备的传感器获取到环境信息由自带的微型系统计算分析,通过WIFI网络连接路由器,接入互联网,将信息传入云端。云端服务器通过3G/4G/5G/6G/WIFI网络,实时地与每个人的手机通信,发送信息。云端还可以接收来自手机客户端下达的各项指令,再通过网络->路由器->WIFI到电器设备,设备接到指令后就可以去执行。比如照明设备可以在用户回家之前自动开启,空调在回家前20分钟自动开启升温或者降温,清洁设备可以自动启动开始除尘,烹饪设备可以开始工作制作食物,饮水设备可以自动加热,配备了自动导航和自动驾驶的智能电动汽车可以在主人下班前自动驾驶到公司楼下等待,等等更多。未来想象空间是无限的,而且这些东西实际上离我们已经很近了。

硬件是科技的载体,软件是科技的灵魂,这些全部离不开编程技术。我们程序员应该考虑用什么技术去实现这些,我的观点是传统的C/C++,Java,Golang都无法满足需求,这些编程语言天生就是为专业的程序而生。未来的编程语言,必须要简单,非计算机专业的人士也可以掌握。个人感觉云端服务器程序,PHP,Python,Ruby都是很好的选择。JavaScript肯定不再其中,目前专业程序用JS都会觉得很饶,这个语言不够严谨。当然传统PHP也是不行的,必须有实时通信支持才可以满足需求,PHP配合Swoole就毫无压力了。移动客户端程序,Swift/Dart是很好的选择。

分类: 互联网 标签:

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(&param, 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, &param TSRMLS_CC);
			zval_ptr_dtor(&param);
			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扩展/C语言/网络编程初学者推荐的几本书

2014年8月5日 没有评论

Linux/Unix系统

  • 深入理解计算机系统
  • UNIX环境高级编程
  • 深入理解Linux内核

网络通信编程

  • UNIX网络编程
  • TCP/IP详解
  • Linux多线程服务端编程

数据结构与算法

  • 算法导论
  • 《数据结构》(C语言版)
  • C程序设计语言

PHP语言

  • PHP5权威编程

 

分类: C/C++ 标签:

回复 《吐槽swoole》

2014年7月28日 1 条评论

看到这篇博文(http://my.oschina.net/u/140911/blog/295022),深感欣慰。有人喷是好事,说的好的地方对我们是巨大的帮助。实话说口水战没意思,我对这个毫无兴趣,只是为了传播的更广。

后面不会再喷Node.js了,主要原因是:

  1. 我不是资深的Node.js用户,而且swoole和Node.js定位不同,发展方向也不同。没办法公平比较
  2. 避免Node.js的粉丝们来攻击我,我也没空余时间来争吵

关于此文中的内容,我针对性的回复一下:

  1. 无论是swoole还是Node.js,都是解决IO密集型问题的。计算密集型的大家开多个进程都可以用到多核。
    但swoole IO密集部分也是可以利用多核的(基于多线程的EventLoop),Node.js不行。
  2. redis,memcache,mongodb的异步化。这个我们后续会支持的。swoole才发展不到2年时间,不可能一下子就全部都支持到。
    另外,复杂的业务逻辑,不建议用异步,同步阻塞就好了。swoole提供的异步MySQL不见得有几个人用。
  3. 异步文件读写,这个我为什么敢喷Node.js,我有资格。Node.js用的4个线程阻塞IO模拟异步,遇到大量并发读写文件时,线程数自然就是瓶颈了,所以Node.js的异步文件API不堪大用。
    而swoole用了Linux Native AIO(需要加编译参数开启),遇到大量并发读写文件,一样可以胜任。
  4. 像Node.js这样全异步回调是好事么?未必。各有各的好处和适用场景。异步的程序很难写,维护性太差。所以偏底层追求性能的就用异步,偏应用层面就用同步阻塞。这也就是swoole和Node.js理念上的最大不同。swoole既支持异步,又可以同步。还可以半异步(EventWorker),半同步(TaskWorker)。

另外:

  1. 就像你说的Node.js不仅仅是Net部分,Swoole也不仅仅是Net部分。
  2. 你没看过Swoole源码,我可是看过Node.js源码的,并且做过大量的分析。可能比你还要懂Node.js底层
  3. 你对我博文吐槽部分,有不少错误的地方

 

分类: 互联网 标签:

关于PHP程序员解决问题的能力

2014年6月24日 3 条评论

这个话题老生长谈了,在面试中必然考核的能力中,我个人认为解决问题能力是排第一位的,比学习能力优先级更高。解决问题的能力既能看出程序员的思维能力,应变能力,探索能力等,又可以看出他的经验。如果解决问题能力不佳是无法通过面试的。

这里举个例子,假如我执行了一个PHP的脚本,如php test.php,预期是可以返回一个字符串。但执行后没有任何信息输出,这时候通过什么方法能知道程序错在哪里?这里可以将解决问题能力分为8个等级,越到后面的表示能力越强。

Lv0 查看PHP错误信息

程序没有达到预期效果,证明代码出错了,看PHP的错误信息是第一步。如果直接忽略错误信息,表明这个人不适合担任专业的程序员岗位。有些情况下php.ini配置中关闭了错误显示,需要修改php.ini打开错误信息,或者错误信息被导出到了日志文件,这种情况可以直接tailf php_error.log来看错误信息。

拿到错误信息后直接定位到程序代码问题,或者到Google/百度搜索,即可解决问题。

注:打开错误显示的方法是

  • php.ini中display_errors / display_startup_errors 设置为On
  • php.ini中error_reporting 设置为E_ALL
  • PHP代码中设置error_reporting(E_ALL)

Lv1 存在多个版本的php或php-cli与php-fpm加载不同的配置

存在多个版本的php,懂得通过which php来看是哪个PHP,或者加绝对路径制定php版本。表示此PHPer通过了此层级的50%考验。

另外一个情况就是php-cli与php-fpm得到的执行情况不一样,如在web浏览器中执行是对的,cli下执行是错的。这时候可能是2个环境加载的php.ini不同所致。cli下通过php -i |grep php.ini得到加载了哪个php.ini。而fpm下通过phpinfo()函数可以得到php.ini的绝对路径。

 

Lv2 var_dump/die打印变量值信息单步调试

这是惯用的程序调试手段,也是最简单粗暴有效的解决问题方法。高级一点的手段是使用PHP的Trace类/日志类,花哨一点的可以借助phpstorm+xdebug在IDE工具里进行Debug。

Trace工具还可以分析脚本的耗时,进行PHP程序的性能优化。

这3个考验全部通过,表明此程序员已经具备了专业PHP程序员应该有的解决问题能力了。PHP程序员只要过了这个等级,就足以应多大部分情况,在中小型网站中毫无压力。

 

Lv3 使用strace工具跟踪程序执行

strace可以用来查看系统调用的执行,使用strace php test.php,或者strace -p 进程ID。strace就可以帮助你透过现象看本质,掌握程序执行的过程。这个手段是在大型网站,大公司里最常用的。如果没掌握strace,这里只能说抱歉了,我们不接受不会strace的PHPer。

strace其实也是对程序员基础的考验,如果不懂操作操作系统,完全不懂底层,肯定也达不到会用strace的程度。当然strace对于PHP代码里的死循环是解决不了的。比如你发现一个php-fpm进程CPU100%了,strace恐怕是解决不了的。因为strace是看系统调用,一般都是IO类操作,既然是IO密集,那CPU一定不可能是100%。

 

Lv4 使用tcpdump工具分析网络通信过程

tcpdump可以抓到网卡的数据通信过程,甚至数据内容也可以抓到。使用tcpdump可以看到网络通信过程是什么样的,如何时发起了TCP SYN3次握手,何时发送FIN包,何时发送RST包。这是一个基本功,如果不懂tcpdump,证明不具备网络问题解决能力。

 

Lv5 统计函数调用的耗时和成功率

使用xhporf/xdebug导出PHP请求的调用过程,然后分析每个函数调用的过程和耗时。能够分析PHP程序的性能瓶颈,找出可以优化的点。

另外一个对于网络服务的调用,如mysql查询,curl,其他API调用等,通过记录起始和结束时microtime,返回的是不是false,可以得到调用是否成功,耗时多少。如果可以汇总数据,整理出调用的成功率,失败率,平均延时,证明此程序员对接口质量敏感,有大型网站项目经验。

 

Lv6 gdb使用

gdb是C/C++调试程序的利器,需要具备一定C/C++功底的程序员才会能熟练使用gdb。上面说的strace无法跟踪php程序CPU100%,而gdb是可以跟踪的。另外gdb也可以解决php程序core dump的问题。

通过gdb -p 进程ID,再配合php-src的.gdbinit zbacktrace等工具,可以很方便地跟踪PHP程序的执行。像上面的CPU100%往往是PHP程序中发生死循环了,gdb进行多次查看,就大致可以得到死循环的位置。具备gdb解决问题能力的PHP程序员少之又少。如果能使用gdb解决PHP问题,这个PHPer百分之百可以通过面试,并且可以拿到较高的技术评级。

 

Lv7 查看PHP内核和扩展源码

如果能熟悉PHP内核和扩展的源码,遇到PHP程序中最复杂的内存错误,也可以有解决的能力。这类PHP程序员就是凤毛麟角了。配合gdb工具和对PHP源码的熟悉,可以查看opcode的信息,execute_data的内存,全局变量的状态等。

 

分类: PHP 标签:

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扩展 标签:

使用GDB调试PHP代码,解决PHP代码死循环

2014年6月1日 没有评论

最近在帮同事解决Swoole Server问题时,发现有1个worker进程一直处于R的状态,而且CPU耗时非常高。初步断定是PHP代码中发生死循环。

下面通过一段代码展示如何解决PHP死循环问题。

#dead_loop.php
$array = array();
for($i = 0; $i < 10000; $i++)
{
    $array[] = $i;
}
include __DIR__."/include.php";

#include.php
while(1)
{
    usleep(10);
    $keys = array_flip($array);
    $index = array_search(rand(1500, 9999), $array);
    $str = str_repeat('A', $index);
    $strb = test($index, $str);
}

function test($index, $str)
{
    return str_replace('A', 'B', $str);
}

通过ps aux得到进程ID和状态如下,使用gdb -p 进程ptrace跟踪,通过bt命令得到调用栈

htf 3834 2.6 0.2 166676 22060 pts/12 R+ 10:50 0:12 php dead_loop.php
gdb -p 3834
(gdb) bt
#0 0x00000000008cc03f in zend_mm_check_ptr (heap=0x1eaa2c0, ptr=0x2584910, silent=1, __zend_filename=0xee3d40 "/home/htf/workspace/php-5.4.27/Zend/zend_variables.c",
__zend_lineno=182, __zend_orig_filename=0xee1888 "/home/htf/workspace/php-5.4.27/Zend/zend_execute_API.c", __zend_orig_lineno=437)
at /home/htf/workspace/php-5.4.27/Zend/zend_alloc.c:1485
#1 0x00000000008cd643 in _zend_mm_free_int (heap=0x1eaa2c0, p=0x2584910, __zend_filename=0xee3d40 "/home/htf/workspace/php-5.4.27/Zend/zend_variables.c", __zend_lineno=182,
__zend_orig_filename=0xee1888 "/home/htf/workspace/php-5.4.27/Zend/zend_execute_API.c", __zend_orig_lineno=437) at /home/htf/workspace/php-5.4.27/Zend/zend_alloc.c:2064
#2 0x00000000008cebf7 in _efree (ptr=0x2584910, __zend_filename=0xee3d40 "/home/htf/workspace/php-5.4.27/Zend/zend_variables.c", __zend_lineno=182,
__zend_orig_filename=0xee1888 "/home/htf/workspace/php-5.4.27/Zend/zend_execute_API.c", __zend_orig_lineno=437) at /home/htf/workspace/php-5.4.27/Zend/zend_alloc.c:2436
#3 0x00000000008eda0a in _zval_ptr_dtor (zval_ptr=0x25849a0, __zend_filename=0xee3d40 "/home/htf/workspace/php-5.4.27/Zend/zend_variables.c", __zend_lineno=182)
at /home/htf/workspace/php-5.4.27/Zend/zend_execute_API.c:437
#4 0x00000000008fe687 in _zval_ptr_dtor_wrapper (zval_ptr=0x25849a0) at /home/htf/workspace/php-5.4.27/Zend/zend_variables.c:182
#5 0x000000000091259f in zend_hash_destroy (ht=0x7f7263f6e380) at /home/htf/workspace/php-5.4.27/Zend/zend_hash.c:560
#6 0x00000000008fe2c5 in _zval_dtor_func (zvalue=0x7f726426fe50, __zend_filename=0xeea290 "/home/htf/workspace/php-5.4.27/Zend/zend_execute.c", __zend_lineno=901)
at /home/htf/workspace/php-5.4.27/Zend/zend_variables.c:45
#7 0x0000000000936656 in _zval_dtor (zvalue=0x7f726426fe50, __zend_filename=0xeea290 "/home/htf/workspace/php-5.4.27/Zend/zend_execute.c", __zend_lineno=901)
at /home/htf/workspace/php-5.4.27/Zend/zend_variables.h:35
#8 0x0000000000939747 in zend_assign_to_variable (variable_ptr_ptr=0x7f7263f8e738, value=0x7f726426f6a8) at /home/htf/workspace/php-5.4.27/Zend/zend_execute.c:901
#9 0x0000000000997ee5 in ZEND_ASSIGN_SPEC_CV_VAR_HANDLER (execute_data=0x7f726d04b2a8) at /home/htf/workspace/php-5.4.27/Zend/zend_vm_execute.h:33168
#10 0x000000000093b5fd in execute (op_array=0x21d58b0) at /home/htf/workspace/php-5.4.27/Zend/zend_vm_execute.h:410
#11 0x0000000000901692 in zend_execute_scripts (type=8, retval=0x0, file_count=3) at /home/htf/workspace/php-5.4.27/Zend/zend.c:1315
#12 0x000000000087926a in php_execute_script (primary_file=0x7ffffe0038d0) at /home/htf/workspace/php-5.4.27/main/main.c:2502
#13 0x00000000009a32e3 in do_cli (argc=2, argv=0x7ffffe004d18) at /home/htf/workspace/php-5.4.27/sapi/cli/php_cli.c:989
#14 0x00000000009a4491 in main (argc=2, argv=0x7ffffe004d18) at /home/htf/workspace/php-5.4.27/sapi/cli/php_cli.c:1365

执行gdb后,死循环的进程会变成T的状态,表示正在Trace。这个是独占的,所以不能再使用strace/gdb或者其他ptrace工具对此进程进行调试。另外此进程会中断执行。gdb输入c后,程序继续向下运行。然后再次按下ctrl + c中断程序。 通过bt命令查看进程的调用栈。


(gdb) bt
#0 _zend_mm_alloc_int (heap=0x1eaa2c0, size=72, __zend_filename=0xe43410 "/home/htf/workspace/php-5.4.27/ext/standard/array.c", __zend_lineno=2719,
__zend_orig_filename=0xee5a38 "/home/htf/workspace/php-5.4.27/Zend/zend_hash.c", __zend_orig_lineno=412) at /home/htf/workspace/php-5.4.27/Zend/zend_alloc.c:1895
#1 0x00000000008ceb86 in _emalloc (size=72, __zend_filename=0xe43410 "/home/htf/workspace/php-5.4.27/ext/standard/array.c", __zend_lineno=2719,
__zend_orig_filename=0xee5a38 "/home/htf/workspace/php-5.4.27/Zend/zend_hash.c", __zend_orig_lineno=412) at /home/htf/workspace/php-5.4.27/Zend/zend_alloc.c:2425
#2 0x0000000000911d85 in _zend_hash_index_update_or_next_insert (ht=0x2257a10, h=3972, pData=0x7ffffe0012b0, nDataSize=8, pDest=0x0, flag=1,
__zend_filename=0xe43410 "/home/htf/workspace/php-5.4.27/ext/standard/array.c", __zend_lineno=2719) at /home/htf/workspace/php-5.4.27/Zend/zend_hash.c:412
#3 0x00000000007767e1 in zif_array_flip (ht=1, return_value=0x7f726424ea68, return_value_ptr=0x0, this_ptr=0x0, return_value_used=1)
at /home/htf/workspace/php-5.4.27/ext/standard/array.c:2719
#4 0x000000000093c03e in zend_do_fcall_common_helper_SPEC (execute_data=0x7f726d04b2a8) at /home/htf/workspace/php-5.4.27/Zend/zend_vm_execute.h:643
#5 0x00000000009400e6 in ZEND_DO_FCALL_SPEC_CONST_HANDLER (execute_data=0x7f726d04b2a8) at /home/htf/workspace/php-5.4.27/Zend/zend_vm_execute.h:2233
#6 0x000000000093b5fd in execute (op_array=0x21d58b0) at /home/htf/workspace/php-5.4.27/Zend/zend_vm_execute.h:410

两次的BT信息不一样,这是因为程序在不同的位置中断。看到execute (op_array=0x21d58b0) 这一行,这里就是PHP执行op_array的入口了。gdb下输入f 6,(通过调用栈编号可得)。


(gdb) f 6
#6 0x000000000093b5fd in execute (op_array=0x21d58b0) at /home/htf/workspace/php-5.4.27/Zend/zend_vm_execute.h:410
410 if ((ret = OPLINE->handler(execute_data TSRMLS_CC)) > 0) {
(gdb) p *op_array
$2 = {type = 2 '\002', function_name = 0x7f726d086540 "test", scope = 0x0, fn_flags = 134217728, prototype = 0x0, num_args = 2, required_num_args = 2, arg_info = 0x7f726d086bd8,
refcount = 0x7f726d0870f0, opcodes = 0x7f726424d600, last = 8, vars = 0x7f726424e890, last_var = 2, T = 1, brk_cont_array = 0x0, last_brk_cont = 0, try_catch_array = 0x0,
last_try_catch = 0, static_variables = 0x0, this_var = 4294967295, filename = 0x7f726424ba38 "/home/htf/wwwroot/include.php", line_start = 12, line_end = 15, doc_comment = 0x0,
doc_comment_len = 0, early_binding = 4294967295, literals = 0x7f726424eae0, last_literal = 4, run_time_cache = 0x7f726450bfb0, last_cache_slot = 1, reserved = {0x0, 0x0, 0x0, 0x0}}

这里的filename就能看到op_array是哪个PHP文件的。然后输入f 0进入当前位置。


(gdb) p **executor_globals.opline_ptr
$4 = {handler = 0x93ff9c , op1 = {constant = 1680133296, var = 1680133296, num = 1680133296, hash = 140129283132592, opline_num = 1680133296,
jmp_addr = 0x7f726424ccb0, zv = 0x7f726424ccb0, literal = 0x7f726424ccb0, ptr = 0x7f726424ccb0}, op2 = {constant = 0, var = 0, num = 0, hash = 0, opline_num = 0, jmp_addr = 0x0,
zv = 0x0, literal = 0x0, ptr = 0x0}, result = {constant = 32, var = 32, num = 32, hash = 32, opline_num = 32, jmp_addr = 0x20, zv = 0x20, literal = 0x20, ptr = 0x20},
extended_value = 1, lineno = 5, opcode = 60 ' 

这里的lineno表示OPCODE所在的代码行数,可以到对应文件里去看下是哪行代码。使用GDB可以查看到更多的信息,这里就不再一一介绍了,有兴趣各位可以自行尝试。

zbacktrace的使用

zend官方提供了一个gdb的脚本,对指令进行了封装,可以直接看到php函数的调用关系。在php源代码包的根目录中有一个.gdbinit。使用


source your_php_src_path/.gdbinit
zbacktrace

可以直接看到PHP函数的调用堆栈。

分类: PHP系统编程, 扩展开发 标签:

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.js, PHP, Swoole扩展 标签:

swoole的进程模型架构

2014年5月5日 没有评论

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扩展, 进程间通信 标签: