zoco

Swoole笔记

2016-12-15


<1>

<2>

腾讯QQ也是有C10K问题的,只不过他们是用了UDP这种原始的包交换协议来实现的,绕开了这个难题。当然过程肯定是痛苦的。如果当时有epoll技术,他们肯定会用TCP。后来的手机QQ,微信都采用TCP协议。实际上当时也有异步模式,如:select/poll模型,这些技术都有一定的缺点,如selelct最大不能超过1024,poll没有限制,但每次收到数据需要遍历每一个连接查看哪个连接有数据请求。既然有了C10K问题,程序员们就开始行动去解决它。于是FreeBSD推出了kqueue,Linux推出了epoll,Windows推出了IOCP。这些操作系统提供的功能就是为了解决C10K问题。因为Linux是互联网企业中使用率最高的操作系统,Epoll就成为C10K killer、高并发、高性能、异步非阻塞这些技术的代名词了。

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

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

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

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

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

PHP的用户函数、运行时局部变量,全局变量,常量都是放在一个Hashtable中。每一条opcode指令都对应一个C函数。

执行一次C函数的开销主要是1:参数的入栈出栈,2:CPU寄存器状态保存。

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

swoole的TCP连接都是以数字的方式提供给PHP端的,在PHP代码中只需要保存fd/from_id这2个数字,即可向对应的连接发送数据。swoole本身也提供了可以遍历所有连接的函数接口(swoole_connection_list/swoole_connection_info)。这两个函数在EventWorker/TaskWorker均可调用。

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

进程的事件循环使用select/poll,因为主线程中的文件描述符只有几个,使用select/poll即可reactor线程/worker进程中使用epoll/kqueue。

协程,coroutine。当程序员还沉浸在解决C10K问题带来的成就感时,一个新的问题被抛出了。异步嵌套回调太TM难写了。尤其是Node.js层层回调,缩进了几十层,要把程序员逼疯了。于是一个新的技术被提出来了,那就是协程(coroutine)。这个技术本质上也是异步非阻塞技术,它是将事件回调进行了包装,让程序员看不到里面的事件循环。程序员就像写阻塞代码一样简单。比如调用 client->recv() 等待接收数据时,就像阻塞代码一样写。实际上是底层库在执行recv时悄悄保存了一个状态,比如代码行数,局部变量的值。然后就跳回到EventLoop中了。什么时候真的数据到来时,它再把刚才保存的代码行数,局部变量值取出来,又开始继续执行。这个就像时间禁止的游戏一样,国王对巫师说“我必须马上得到宝物,不然就砍了你的脑袋”,巫师念了一句时间停止的咒语,直到过了1年后勇士们才把宝物送来。这时候巫师解开咒语,把宝物交给国王。这里国王就可以理解成协程,他根本没感觉到时间停止,在他停止到醒来期间发生了什么他不知道,也不关心。这就是协程的本质。协程是异步非阻塞的另外一种展现形式。Golang,Erlang,Lua协程都是这个模型。

用3个队列来实现异步代理调用。工作队列、空闲队列、等待队列。

<3>

Swoole没有采用多线程模型而是使用的多进程模型,在一定程度上减少了访问数据时加锁解锁的开销,但同时也引入了新的需求:共享内存。

Swoole中为了更好的进行内存管理,减少频繁分配释放内存空间造成的损耗和内存碎片,Rango设计并实现了三种不同功能的MemoryPool:FixedPool,RingBuffer和MemoryGlobal。

虽然原来也知道结构体中可以通过存放函数指针模拟一个类,但是现在才学到可以通过传入一个指针参数来模拟this指针,使结构体更像一个类。

FixedPool是随机分配内存池(random alloc/free),将一整块内存空间切分成等大小的一个个小块,每次分配其中的一个小块作为要使用的内存,这些小块以链表的形式存储。 

RingBuffer。这相当于一个循环数组,每一次申请的一块内存在该数组中占据一个位置,这些内存块是可以不等长的,因此每个内存块需要有一个记录其长度的变量。

Pipe(管道)用于进程之间的数据交互,Linux系统本身提供了pipe函数用于创建一个半双工通信管道,而在swoole中也利用eventfd和unix sock封装了两种管道,使得进程间的通信更加灵活。

Reactor模块可以说是Swoole中最核心的模块之一,正是这些reactor模型为swoole提供了异步操作的基础。Swoole中根据不同的内核函数,提供了四种Reactor封装,ReactorEpoll,ReactorKqueue,ReactorPoll和ReactorSelect。同时,Swoole通过结构体swReactor封装了对于reactor的操作函数和基本属性。

epoll是Linux内核提供的一个多路复用I/O模型,它提供和poll函数一样的功能:监控多个文件描述符是否处于I/O就绪状态(可读、可写)。这就是异步最核心的表现:程序不是主动等待一个描述符可以操作,而是当描述符可操作时由系统提醒程序可以操作了,程序在被提醒前可以去做其他的事情(这里的程序、描述符、系统可以更换为其他东西) Linux提供了三个主要的系统调用:epoll_create,epoll_ctl,epoll_wait。 epoll_create用于创建一个epoll实例并返回这个实例的文件描述符。epoll_ctl用于将一个需要监控的文件描述符在epoll中注册对应的监听事件,该函数也可用于更改一个已注册描述符的监听事件。epoll_wait函数用于等待监听的描述符的I/O事件,如果所有描述符都没有就绪,该函数会阻塞直到有至少一个描述符进入就绪状态。(该段描述翻译自 Linux 命令:man epoll )

请说明一下epoll函数的水平触发(Level-triggered)和边缘触发(edge-triggered)两种模式的区别:水平触发和边缘触发是epoll的两种模式,它们的区别在于:水平触发模式下,当一个fd就绪之后,如果没有对该fd进行操作,则系统会继续发出就绪通知直到该fd被操作;边缘触发模式下,当一个fd就绪后,系统仅会发出一次就绪通知。

Factory这个命名让我一度认为这是一个工厂模型……这个工厂实际上并不负责生产实例,而是根据类型的不同执行两项任务:Factory实现的功能是一个任务中心,一个task请求进入Factory,会进过dispatch分配、onTask处理、onFinish交付结果一系列流程;FactoryProcess用于管理manager和worker进程,也有对单独的writer线程的管理。 PS:Swoole源码中有FactoryThread模块,该模块是一个多线程模型,根据开发者Rango的解释,因为PHP不支持多线程,所以无法使用这个模块,因此该模块被废弃了。而实际上,FactoryThread比FactoryProcess要更简洁……)

FactoryProcess模块 FactoryProcess模块是Swoole的进程管理模块,是Swoole另一个核心组件。通过该模块,Swoole能有效的调度和管理Master进程和多个Worker进程。 

Worker在Swoole中为核心工作进程的封装,包括用于处理核心逻辑的worker和用于处理任务的task_worker。

一个Worker中有两个管道,一个管道用于和master进程通信,一个管道用于和Reactor通信。而实际上如果指定了消息队列模式,则通信方式都是通过读写swQueue队列来实现的。

swTimer结构体定时器的实体对象,用于存储、管理和执行众多定时任务,包括timer和after两种不同类型的定时任务。

EventTimer的实现原理是利用了epoll的timeout超时设置。通过设置epoll的timeout,就能在timeout时间后捕获一个事件,在捕获该事件后,通过遍历对应的事件列表即可得知哪些事件需要处理。

条件变量(Cond)的用处是使线程睡眠等待某种条件出现后唤醒线程,是利用线程间共享的全局变量进行同步的一种机制,主要包括两个动作:一个是线程等待“条件成立”而挂起,另一个是线程使“条件成立”并发出信号。为了防止竞争,条件变量通常与一个互斥锁结合在一起。

<4>

<5>

安装

sudo apt-get install php5 php5-dev
suod apt-get install gcc autoconf
git clone [https://github.com/swoole/swoole-src.git](https://github.com/swoole/swoole-src.git)
cd swoole-src
phpize(利用autoconf来生成一个.configure文件,.configuer实际上是一段shell脚本,用来检测系统的版本之类的,查看系统的头文件,epoll,kqueue等是否存在)
./configure --help (可以查看swoole支持的一些东西,比如:--enable-swoole-debug)
不添加任何参数的话./configure
生成一个MakeFile文件,进行编译make将所有文件link到一个swoole.so文件
sudo make install
修改php.ini(查看php.ini的位置php -i|grep php.ini)
在php.ini最后加上一行extension=swoole.so
在terminal输入php -m,会看到swoole

PHP C扩展sudo apt-get install gcc make autoconf

cd php-src/ext
./ext_skel --extname=test会多出一个test文件
cd test
修改config.m4
把config.m4中的dnl PHP_ARG_WITH(test,for test support,和dnl [ --with-test Include test support])中的dnl注释掉(dnl在config.m4中是注释的意思)就可以启用这个扩展了
phpize
./configure
make
sudo make install
找到php.ini文件(通过php -i|grep php.ini来找到)
vim 找到的php.ini文件
在最下面添加extension=test.so
执行php -m会发现多了一个test
写一个php文件来测试
ob_start();
include_once($filename);
ob_get_content();
ob_clean();

就可以了