Swoole笔记
2016-12-15
<1>
- manager是进程,worker是进程,task是进程,master是线程,reactor是线程,心跳检测是线程,UDP收发是线程
- reactor和worker之间的通信是通过IPC实现的
- 和worker进行通信有两种方式:管道和消息队列
- 主进程mainReactor负责监听server socket
- tcp分为nopush和nodelay两种方式
- 主进程mainReactor:
- 负责监听server socket,评估每个reactor线程的连接数量,评估方式也就是分配的方式是通过fd%serv->reactorNum实现的
- 将监听到的accept请求分配给连接数最少的reactor线程
- 接管所有信号的signal处理,使reactor线程运行中不受到打扰
- 管理进程manager
- 分为worker进程和taskWorker进程
- 所有的worker进程和task进程都是manager进程fork出来的
- 当worker进程发生致命错误或者运行生命周期结束时,管理进程会回收此进程,并创建新的进程,防止子进程成为僵尸进程
- 管理进程可以平滑重启所有worker进程,以实现程序代码的重新加载
- 异步reactor线程(全异步非阻塞)
- 收发数据,处理网络I/O
- 处理TCP连接,将发来的数据缓冲拼接,拆分成完整的请求包
- 负责监听从mainReactor分配来的socket
- socket可读时读取数据,进行协议解析,然后将数据投递到worker进程,在socket可读时将数据发给TCP客户端
- 同步或者异步worker进程,没有用到epoll
- 处理数据
- 接受由reactor线程投递的请求数据包,并执行PHP回调函数处理数据
- 生成响应数据并发给reactor线程,由reactor线程发给客户端
- task worker进程(完全是同步阻塞模式)
- 有些逻辑代码不需要马上执行,可以将一个task任务投递到taskworker进程池,在worker进程空闲时再去捕获任务执行的结果。
- factory<->task
- 不生产实例
- 根据类型的不同执行
- 任务中心,一个task请求进入factory,会通过dispatch分配,onTask处理,onFinish交付结果一系列流程
- FactoryProcess用于管理manager和worker进程,也会对单独的writer线程管理
- 如果reactor最大允许监听的事件数比reactor的事件数小的话用poll/select,否则用epoll/kqueue
- client的类型:TCP,TCP6,UDP,UDP6,UNIXSTREAM,UNIXDGRAM
- client
- 异步:socket从mainReactor中获取
- 同步:直接创建一个connection
- DNS:只可以是异步的查询,hashMap
<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>
- 基于swoole的AsyncTask模块实现的连接池是完美方案,编程简单,没有数据同步和锁的问题。甚至可以多个服务共享连接池。缺点是1, 灵活性不如多线程连接池,无法动态增减连接。2, 有一次进程间通信的开销。— 这里的甚至可以多个服务共享连接池。
- 代码统计cloc和gitstats(有web显示)
- 只有异步非阻塞方式,没有异步阻塞方式。
- 用了Epoll任何程序都能扛住100W并发连接,只是占内存多少的问题了。
- 如果用共享内存,又存在安全问题。$i++ 这种操作不是线程安全的。
- $server->connections 是一个迭代器对象,所以只能foreach,你var_dump它就是一个空的对象而已
- task操作的次数必须小于onTask处理速度,如果投递容量超过处理能力,task会塞满缓存区,导致worker进程发生阻塞。worker进程将无法接收新的请求
- 它会自动判断浏览器类型,如果支持websocket就会使用WebSocket,不支持WebSocket就会用JS Comet
- Comet的缺点是它要创建2个TCP连接。
<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
-
linux一个重要的分界线是2.6.27,在2.6.27很多都不支持(timerfd,epoll)
-
查看linux版本uname -a
-
nm 可执行文件(比如nm .a.out)可以看到这个可执行文件都定义了哪些函数
-
telnet ip port就可以登陆telnet,例如:telnet 127.0.0.1 9501
-
0.0.0.0是监听所有的端口
-
滑动窗口是用来保证发的包是有序的
-
路由器之间交换信息:BGP,OSPF
-
网管就是出口路由器,如192.168.1.1
-
socket/内核/网卡网卡收发数据后,发生网络中断,由内核处理结果
-
内核将应用层的内存数据写入网卡
-
并将网卡传入的数据拷贝到应用层的内存中
-
read/write中的内存地址
-
内核为每一个TCP连接创建文件描述符
-
Accept/Close创建文件描述符,关闭连接
-
广播地址4个字段的最后一个字段是255,例如192.168.1.255
-
DNS通过UDP来实现的
-
tcpdump中FLAGS是S代表TCP中的握手同步,P代表数据推送,R代表对方还没有收到数据就被关闭了。
-
sudo tcpdump -iany udp port 53
-
线程之间是可以共享fd的
-
UDP特点:包式协议,有消息边界,一次必然是一个数据包;每个数据包最大长度不超过64K;不保证可靠性,数据包有可能会丢失;不保证顺序,多个数据包的顺序可能是反的。
-
netcat可以在linux下取代telnet做一些简单的客户端测试
-
UDP服务器适合的场景:允许小部分数据丢失,如统计和日志;实时性要求高,比如视频流,丢一部分帧也没关系(TCP在保证可靠性的前提下,实时性方面可能不是很好。UDP允许丢失,反而会具备流量过载保护的能力,如果程序自行保证可靠性(丢失重传)和顺序(包排序),可以无视UDP存在的问题
-
TCP通信的特点:流式协议,没有消息边界,客户端向服务器发送一次数据,可能会被服务器端分成多次收到。客户端向服务器发送多次数据,服务器可能一次全部收到;保证传输可靠性,顺序;TCP有拥塞控制,所以数据包可能会延后发送。
-
swoole提供两种通用TCP协议解析:EOF检测(只解决数据包合并,不解决拆分),在数据发送结尾加入特殊字符(如\r\n\r\n),表示一个请求传输完毕(一定会收到完整的包,但无法解决可能服务器会收到多个完整的问题,有可能会收到多个完整的包);
-
固定包头+包体(解决了数据合并和拆分),协议设计成固定包头+包体,并且包头内有一个固定字节数的长度,不支持变长字节长度。
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文件来测试
- php -rf 函数名 会看看这个函数是否存在
- 写http_sever的时候处理静态请求直接file_get_content($pathinfo)就可以了,处理动态请求根据请求文件的后缀来处理,然后
ob_start();
include_once($filename);
ob_get_content();
ob_clean();
就可以了