关于网络I/O,nginx原理,php-fpm处理并发数
一、网络I/O
1、就是建立网络连接,然后数据传输,对于服务器而言,客户端对我的请求为Input,我对客户端的响应为Output;对于客户端而言则相反,我们这里讨论服务端。
对于网络连接(TCP/IP)来说,本质上是基于socket网络套接字(文件描述符fd)来的,客户端和服务端各自建立一个fd来实现通讯,并且如果是http请求,响应完fd就被释放,因此会频繁的创建与销毁。如果多个客户端连接了服务器,则服务器上有多个fd,fd可以理解为一个临时的带有标识的资源,客户端1向服务端发数据时,服务器底层会把数据写入到fd1,并伴有fd1的其他属性的变化,这样服务端就知道有请求来了,就会处理并响应出去,然后客户端1的fd就会有所变动,客户端就会据此作出反应;同样,如果客户端1向服务端同时发出三个请求(http),因为http是无状态的,所以,客户端有fda,fdb,fdc,对应服务端fd1a,fd1b,fd1c,基于此,我们可以看到,浏览器内核使用了事件注册,请求队列,响应回调的机制来处理这些请求,避免了阻塞;而服务端(早期),默认情况下,一个进程一次只能处理一个fd,并且是阻塞的方式,先来先处理,在线等待的策略,造成了很大的阻塞,效率低下,因此,无法抗住并发。
2、以下是简单的服务端和客户端
Server端
<?php
set_time_limit(0);
$ip = '127.0.0.1';
$port = 8888;
static $connList = array();
// 1. 创建
if( ($sock = socket_create(AF_INET, SOCK_STREAM, SOL_TCP)) == FALSE ){
echo 'create fail:' . socket_strerror(socket_last_error());
}
// 2. 绑定
if ( socket_bind($sock, $ip, $port) == FALSE ) {
echo 'bind fail:' . socket_strerror(socket_last_error());
}
// 3. 监听
if( socket_listen($sock, 4) == FALSE ){
echo 'listen fail:' . socket_strerror(socket_last_error());
}
$count = 0;
do{
// 4. 阻塞,等待客户端请求
if ( ($msgsock = socket_accept($sock)) == FALSE ) {
echo 'accept fail:' . socket_strerror(socket_last_error());
break;
} else {
array_push($connList,(int)$msgsock);
// 5. 向客户端写入信息
$msg = 'server response '.(int)$msgsock;
socket_write($msgsock, $msg, strlen($msg));
// 5. 读取客户端信息
$buf = socket_read($msgsock, 8192);
$talkback = $buf.PHP_EOL;
echo $talkback;
print_r($connList);
echo PHP_EOL;
}
// 6. 关闭socket
//socket_close($msgsock);
}while(true);
// 6. 关闭socket
socket_close($sock);
3、Client端
<?php
error_reporting(E_ALL);
set_time_limit(0);
$ip = '127.0.0.1';
$port = 8888;
// $ip = '192.168.1.210';
// $port = 7002;
// 1. 创建
$socket = socket_create(AF_INET, SOCK_STREAM, SOL_TCP);
if( $socket == FALSE ) {
echo 'create fail: ' . socket_strerror(socket_last_error());
}
// 2. 链接
$result = socket_connect($socket, $ip, $port);
if ( $result == FALSE) {
echo 'connect failed...'.PHP_EOL.PHP_EOL;
}else{
echo 'connect success...'.PHP_EOL.PHP_EOL;
}
$in = 'student '.(int)$socket.' come in';
// 3. 向服务端写入
if( !socket_write($socket, $in, strlen($in)) ) {
echo 'write fail: ' . socket_strerror(socket_last_error());
}
// 3. 从服务端读取
while ( $out = @socket_read($socket, 8129) ) {
echo $out.PHP_EOL;
}
// 4. 关闭
// echo 'close socket...'.PHP_EOL;
// socket_close($socket);
// echo 'closed ok....';
注释:
1、此处演示的是tcp长连接,不是http,类似。
2、socket_accept 和 socket_connect返回值都是fd资源,(int)fd 可取的资源id,每个连接的id不一样,因此我们就可以据此管理连接了。
此处的server为单进程(当前进程中)的,一次只能响应一个请求。当然,可以 pcntl_fork() 出多个子进程,也是一种简陋的方式,因为频繁的创建销毁进程会造成很大的开销。
二、I/O多路复用和nginx原理
此处以 nginx 作为web服务器与php通讯为场景,request->nginx->php-fpm->nginx->response,其中一部分 nginx->php-fpm->nginx
为了解决服务端的这种阻塞问题,慢慢衍生了I/O复用的思想,即,多个I/O可以复用一个进程。I/O多路复用允许进程同时检查多个fd,以找出其中可执行I/O操作的fd。 系统调用select()和poll()来执行I/O多路复用。在Linux2.6内核中引入的epoll()是select()的升级版,提供了更高的性能。通过I/O复用,我们可以在一个进程处理大量的并发I/O。
初级版I/O复用
比如一个进程接受了10000个连接,这个进程每次从头到尾的问一遍这10000个连接:“有I/O事件没?有的话就交给我处理,没有的话我一会再来问一遍。”然后进程就一直从头到尾问这10000个连接,如果这10000个连接都没有I/O事件,就会造成CPU的空转,并且效率也很低
高级版I/O复用
epoll()
epoll能更高效的检查大量fd,UNIX中提供了类似功能的kqueue调用。epoll可以理解为event poll,不同于忙轮询和无差别轮询,当连接有I/O流事件产生的时候,epoll就会去告诉进程哪个连接有I/O流事件产生,然后进程就去处理这个事件。此时我们对这些流的操作都是有意义的。
也就是,在nginx和php-fpm之间增加了代理epoll,epoll来管理nginx这边的fd,因此可以显著提高nginx的性能和并发数,
Nginx配置use epoll后,以异步非阻塞方式工作,能够轻松处理百万级的并发连接。
events {
worker_connections 1024;
use epoll;
}
虽然如果nginx可处理百万级并发,但是php却扛不住啊,所以需要配置php服务器的负载均衡
server外面
upstream myfastcgi {
# ip_hash; upstream支持的负载均衡算法,默认是轮训,可不用设置
server 192.168.83.128 weight=1; # 默认80端口,192.168.83.128:80
server 192.168.83.129 weight=1;
server 192.168.83.131 weight=1;
}
server里面
location / {
# 转发
proxy_pass http://myfastcgi;
}
nginx也是基于事件注册,异步回调的机制来优化请求的。
ps -ef |grep nginx
会看到一个 master process 和 若干个 worker process,其中 master process 就是我们启动的nginx程序,它用来管理和调度worker进程,包含:接收来自外界的信号,向各worker进程发送信号,监控worker进程的运行状态等,根据并发数的大小master会动态的生成和销毁worker。
三、php-fpm原理
apache是在配置文件中引入了php脚本的解释程序,启动apache的时候就被加载都内存中。
nginx则实现了解耦,通过proxy_pass转发的方式将请求转发到进程管理器php-fpm,安排子进程去调用php程序解释代码,并返回响应。
1、关于php-fpm
ps -ef |grep php-fpm
看到一个master process调度进程和n个pool www工作进程,这点和nginx相似。
在php-fpm.conf配置文件中
pm.max_children:静态方式下开启的php-fpm进程数量。
pm.start_servers:动态方式下的起始php-fpm进程数量。
pm.min_spare_servers:动态方式下的最小php-fpm进程数。
pm.max_spare_servers:动态方式下的最大php-fpm进程数量。
pm = dynamic:执行方式设置成了动态,static静态,如果设置为静态,则子进程数固定为pm.max_children,一般会设置为动态。
2、php-fpm工作原理(动态模式下)
php-fpm并没有实现I/O复用模式,也就是说,是以阻塞的形式来处理请求的,一个进程一次只处理一个请求,并且要等待其处理完。也就是说,此刻有20个子进程,就只能同时处理20个请求了。
启动时有pm.start_servers个子进程,当并发加大时,master就会动态创建一些子进程,创建的数量当然是按照它的机制来的,当并发降低时,master又会销毁一些进程,直至pm.min_spare_servers个,继而pm.start_servers个。
由此可见,基于这种阻塞的模式,注定是没法抗住大的并发,进程数设置的太高必定导致服务器承受不住。
进程和线程
进程:分配独立的内存空间,因此进程数是有上限的。
线程:在进程内,可以创建多个线程,在当前进程里并发运行,共享该进程下的内存地址空间,因此,线程之间并非独立的,存在一定的数据安全性,会共享该内存下的所有内容,其次,当不同的线程需要占用同一个变量时,根据先到先得的原则,先到的线程在运作时,后来的线程只能在旁边等待,也就是加入到了阻塞排队序列。所以这就是造成线程阻塞的原因。
3、建议
a、需要配置负载均衡,多配置几台服务器。
a、因为PHP—FPM只是PHP官方实现的fast-cgi进程管理器,可以不用它;自己写http服务器来提供非阻塞,异步回调,事件注册机制的服务,比如
swoole https://wiki.swoole.com/wiki/page/326.html
easyswoole
以下是一个http server demo:
https://github.com/liuzhang/demo/tree/master/httpd
使用:
php demo.php start
可以使用ab工具来对比它和php-fpm的请求并发情况。