关于网络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的请求并发情况。