swoole学习(二) ---- 手写一个预派生子进程模式的网络服务器
预派生子进程模式的引入 — php-fpm:
我们知道高并发请求时,fpm-worker不够用,nginx直接响应502,当我们达到一定并发时,最简单粗暴的办法就是增加php-fpm的进程数目,可以简单的查看一下当前的php-fpm进程数目,
ps -ef |grep fpm
只有两个子进程
打开我们的fpm配置文件,将最大进程数设置为8个,并重启fpm
我们熟悉的php-fpm的设计就是一个预派生子进程模式,我在上一篇的博客中(单进程阻塞服务器)介绍的模型一次只能处理一个请求,在实际的业务中是比较少见的,因此我们需要加大它的并发能力,怎么加大呢?既然一个进程只能处理一个请求,那么我们就预先给他多生成N个子进程,那多个请求一起来的时候不就可以一起处理了吗?即便需要等待处理,等待的时间不也变短了吗?
知识预备:
pcntl函数(http://php.net/manual/zh/intro.pcntl.php)
设计流程:
- 创建一个socket,绑定服务器端口(bind),监听端口(listen);
- 通过
pcntl_fork
函数创建N个子进程 - 每一个子进程创建成功后都去阻塞监听新的客户端连接
- 当客户端连接到服务器时,其中一个子进程被唤醒,开始处理客户端请求,并 且不再接受新的连接
- 当此连接关闭时,子进程会释放,重新进入 监听客户端,参与处理新的连接
代码实现(windows不支持pcntl扩展,只能在linux下运行)
<?php
class Worker{
//监听socket
protected $socket = NULL;
//连接事件回调
public $onConnect = NULL;
//接收消息事件回调
public $onMessage = NULL;
public $workerNum=4; //子进程个数
public function __construct($socket_address) {
//监听地址+端口
$this->socket=stream_socket_server($socket_address);
}
public function start() {
//获取配置文件
$this->fork(); //创建多个子进程负责接收请求的
}
public function fork(){
for ($i=0;$i<$this->workerNum;$i++){
$pid=pcntl_fork(); //创建成功会返回子进程id
if($pid<0){
exit('创建失败');
}else if($pid>0){
//父进程空间,返回子进程id
}else{ //返回为0子进程空间
$this->accept();//子进程负责接收客户端请求
}
}
//放在父进程空间,结束的子进程信息,阻塞状态
$status=0;
$pid=pcntl_wait($status);
echo "子进程回收了:$pid".PHP_EOL;
}
public function accept(){
//创建多个子进程阻塞接收服务端socket
while (true){
$clientSocket=stream_socket_accept($this->socket); //阻塞监听
var_dump(posix_getpid());
//触发事件的连接的回调
if(!empty($clientSocket) && is_callable($this->onConnect)){
call_user_func($this->onConnect,$clientSocket);
}
//从连接当中读取客户端的内容
$buffer=fread($clientSocket,65535);
//正常读取到数据,触发消息接收事件,响应内容
if(!empty($buffer) && is_callable($this->onMessage)){
call_user_func($this->onMessage,$clientSocket,$buffer);
}
fclose($clientSocket); //必须关闭,子进程不会释放不会成功拿下进入accpet
}
}
}
$worker = new Worker('tcp://0.0.0.0:9800');
//连接事件
$worker->onConnect = function ($fd) {
//echo '连接事件触发',(int)$fd,PHP_EOL;
};
//消息接收
$worker->onMessage = function ($conn, $message) {
//事件回调当中写业务逻辑
//var_dump($conn,$message);
$content="我收到你的请求了";
$http_resonse = "HTTP/1.1 200 OK\r\n";
$http_resonse .= "Content-Type: text/html;charset=UTF-8\r\n";
$http_resonse .= "Connection: keep-alive\r\n"; //连接保持
$http_resonse .= "Server: php socket server\r\n";
$http_resonse .= "Content-length: ".strlen($content)."\r\n\r\n";
$http_resonse .= $content;
fwrite($conn, $http_resonse);
};
$worker->start(); //启动
cli模式运行
检查一下进程数目,ps -aux|grep service.php
没问题,5个进程(1个主进程,4个子进程)
测试:来一波大的,3W个请求,并发300,够大了吧?(真的够大了,服务器是1G的运行内存,子进程只设置了4个,请求时保持长连接,注意-k参数,这个很重要)
完全没问题,如果单进程阻塞模式运行结果是这样的
50个请求2个并发都不行了有没有?太可怜了。。。
注意点:
- pcntl_fork是创建了一个子进程,父进程和子进程 都从fork的位置开始向下继续执行,不同的是父进程执行过程中,得到的fork返回值为子进程 号,而子进程得到的是0,所以阻塞监听的逻辑应该是在返回0的时候执行的
- 僵尸进程的处理,使用pcntl_wait回收子进程
还是存在缺点:预派生子进程模式虽然可以解决并发的问题,但是资源始终是有限的,我们总不能设置10w个进程来维持10w人的链接吧,尽管通过pcntl_wait不断回收,但这个模式太依赖进程的数量了;而且操作系统生成一个子进程需要进行内存复制等操作,在资源和时间上会产生一定的开销;当有大量请求时,会导致系统性能下降;