13

NIO、BIO、AIO 与 PHP 实现

 3 years ago
source link: https://segmentfault.com/a/1190000022356197
Go to the source link to view the article. You can view the picture content, updated content and better typesetting reading experience. If the link is broken, please click the button below to view the snapshot at that time.

前言

最近看到NIO,AIO,Netty,Promise话题很热,我作为一个phper也想来凑凑热闹,凑着凑着发现周围怎么都是javaer,jser。那么 PHP 能做 NIOAIO 么?

什么BIO、NIO、AIO

BIO 同步阻塞I/O。

有小伙伴又要问了啥叫 同步 ,啥叫 阻塞 啊?

同步/异步 阻塞/非阻塞

同步:两个同步任务相互依赖,并且一个任务必须以依赖于另一任务的某种方式执行。 比如在 A->B 事件模型中,你需要先完成 A 才能执行B。 再换句话说,同步调用种被调用者未处理完请求之前,调用不返回,调用者会一直等待结果的返回。

异步:两个异步的任务完全独立的,一方的执行不需要等待另外一方的执行。再换句话说,异步调用种一调用就返回结果不需要等待结果返回,当结果返回的时候通过回调函数或者其他方式拿着结果再做相关事情,

阻塞:阻塞就是发起一个请求,调用者一直等待请求结果返回,也就是当前线程会被挂起,无法从事其他任务,只有当条件就绪才能继续。

非阻塞:非阻塞就是发起一个请求,调用者不用一直等着结果返回,可以先去干其他事情。

以上就是这四个词汇的解释,那么放到计算机IO上,比较接地气的解释

BIO (Blocking I/O)

那么我们拿快递揽件来举例,一个快递公司,有一部分工作是揽件,它的工作模式是只能一个一个的揽件,你要寄快递,必须排队,一个一个的来,这就是 同步 。好不容易轮到你了,你把快递一扔给他,他还让给你等着,快递工作人员说,我们这后面还有些信息要录入,快递要检查,必须等我们快递公司检查完毕后,你才能离开,这叫 阻塞

NIO (No-Blocking I/O)

同步非阻塞的I/O

继续啊,拿快递公司举例。这个快递公司发现有些用户在后面排队,排着排着,太久了就去隔壁快递公司了,怎么办呢?快递公司想了个办法,置办了一个发号器和一批收纳盒。来一个客户,就把快递放在一个收纳盒里,再给用户一个编号,此时再来一个用户,不论前面一个的快递是否检查完毕,还是给他一个收纳盒,发一个编号。不同客户之间不排队,一来就被受理了,这就是 非阻塞 。 我们再来看看内部,快递呢还是一个个地录入信息,X光检查,这样就是 同步 运行的,等待快递人员检查完毕叫号,客户拿到回执才能离开快递点。

AIO (Asynchronous I/O)

异步非阻塞IO

也有Javaer叫他 NIO2,快递公司揽件又升级了,做了一个快递柜,客户又寄件需求,来了就放入快递柜,然后通过手机扫码关注这个柜子的动态,客户就可以离开了,此时服务被受理,并能马上离开。这就是 非阻塞 。等到快递人员来揽件时,会将柜子里面的寄件一并取走,快递点集中一起处理这些快递件,发现有问题的件,不是立即停下手中的活等待客户来出来,而是放一旁通知客户来,然后继续处理下一个快递,这就是 异步

异步 阻塞 IO

同步/异步 阻塞/非阻塞,这4个名词,两两组和,还有一个就是 异步/阻塞

那么我们还是先把例子举出来吧,还是这个快递点,来了一批客户来寄口罩到国外,由于有很大的可能会通不过检查,所以,快递点把大家都留了下来。等所有的 寄件 都检查完了在统一给大家发送回执单,这就是 阻塞 。快递人员检查寄件时,发现问题不是立马通知客户来处理,而已放到一边,继续处理下一个。 这就是 异步

伪异步 IO

这种模式,底层实现是多个 同步阻塞的BIO , 同时运行。

最后总结一下:

阻塞与非阻塞指的的是当不能进行读写(网卡满时的写/网卡空的时候的读)的时候, I/ O操作立即返回还是阻塞;同步异步指的是,当数据已经 ready的时候,读写操作是同步读还是异步读,阶段不同而已。

区别

异步/同步在计算机区别

以上是一些举例,只是帮助大家理解记忆,接下来我们看看计算上的实现。

计算机提供的Web服务,刚开始的 CGI 模式,就是纯正的 BIO 模式。一个 cgi 进程监听一个端口,处理完一个请求,才能接收下一个http请求。这就是 同步

实际使用体验式异步的,那是因为后来优化了, CGI 能够自我fork进程的达到同时响应多个 http 请求的效果。

注意,我们这里讨论的基础是 单进程 ,上的 异步/同步

阻塞/非阻塞在计算机区别

这里拿购物流程举例,用户的下单,需要做如下操作:

  • 商品可售否
  • 库存数量
  • 用户余额
  • 触发哪些优惠规则
  • 奖券有效性众

按照一般做法就是一步步验证,上一个检查完成再进行下一个检查,这就是 阻塞 的方式。

那么非阻塞方式如何做呢,假设在微服务环境中,商品,库存,奖券,促销都是独立的系统,调用商品服务,发起商品可售检查请求;不等商品服务回复,继续调用库存服务,发起商品可售库存请求;紧接着依次发出...检查请求,这样5个检查项目同时发起,最后,我等他们所有的请求都回复我,再来校验是否所有的检查都通过了。就这种发起请求不等响应,就继续做下一件事的叫 非阻塞

转载著名来源sifou

PHP 能做什么

PHP 与 BIO 实现

PHP已经实现啦,这是最基本的好么。但我们平时测试时却感觉是不阻塞啊,你可以试试吧nginx和php-fpm的进程限制为1个试试。其实php-fpm就是 多进程的BIO。

  • 调整Nginx配置

调整 /etc/nginx/nginx.conf 文件:

## 把nginx worker数量设置为1
worker_processes 1;

好了之后我们通过ps命令检查下

qauuUrY.png!web

  • 调整PHP配置

调整 /etc/php/php-fpm/conf.d/www.conf 文件:

pm = static

pm.max_children = 1

pm.start_servers = 1

pm.min_spare_servers = 1

pm.max_spare_servers = 1

找到这几个配置都改为如上数值。

最后的结果如下

z6RfyaJ.png!web

我在 index.php 代码里面加入第一行就加入了sleep。

<?php
sleep(5);

我们同时打开两个网页,一起访问试试

32Qve2z.png!web

通过Firefox 抓包可以发现,其中一个耗时5s,另一个页面耗时9.3s, (0.7s误差是我手速慢了) 这就是 BIO。

zyIfMjQ.png!web

好的,我们在做一个实验。把以上nginx,php-fpm配置中 1 改成 2 .然后我们打开 三个网 页, 同时访问 试试看。

FJNBba2.png!web

结果是有 两个网页耗时5s一个是9s ,也就是说服务器同时处理了2个请求,第三个请求等待了4s才被处理。这就是 多线程-BIO,一个服务同时接待的客户数量取决与worker的数量。

PHP 与 NIO 实现

我们写的大部分php-fpm代码都是非阻塞的。其实PHP是支持 非阻塞 IO编程的。

我们来看看PHP原生代码实现NIO编程:PHP回顾之socket编程。这段代码为了实现并发使用了 stream_select()

I/O 多路复用

这里,PHP 实现 NIO 核心就是 stream_select()

通过以上源码,发现原生的NIO实现还是比较繁琐,不易读的。那么 NIO 就是为了实现一个socket server么,我们来看看 Netty 官网 。打开Netty首页,它是这样描述自己的

Netty is a NIO client server framework which enables quick and easy development of network applications such as protocol servers and clients. It greatly simplifies and streamlines network programming such as TCP and UDP socket server.

第一句话:Netty是一个 NIO 客户端 服务框架, 能快速轻松地开发协程 客户端 。第二句话:简化了网络编程,如创建TCP和UDP套接字服务。

好,重点是什么?第一句话就是重点——开发 协程 客户端 !回到我们业务上,刚刚举了一个例子,购物到下单,有很多个流程需要做检查,按照一般的BIO那么程序时序图如下:

3euQjy7.png!web

从上可以看到,三个检查依次分开执行。那么客户的等待时间是大于,库存检查时间加上,产品检查时间加上,促销检查时间 的。

假设, 库存,产品,促销是三个微服务,然后购物车服务用 NIO客户端 ,与这三个微服务交互,那么会是怎样的效果呢:

mYBZjun.png!web

这里,我们发起检查请求时,是按照顺序发起的,但不等第一个服务返回检查结果就开始发起下一个检查请求。最后三个服务都返回后,综合结果,返回给用户。那么这三个检查的耗时,就等于一个服务(耗时最长的那个服务)的检查耗时。大大减少得了购物车服务响应时间。

NIO 客户端

看到以上两个时序图,还是给大家演示一下用PHP原生代码实现一个 PHP-BIOPHP Simple NIO Server

建议大家点击链接,把源码 git clone https://gitee.com/xupaul/php-nio-server 到本地运行一下,再来看截图更容易理解。

F73AvaM.png!web

这三个所依赖的服务响应耗时,我设置为:inventory: 4s, product: 2s, promo:6s

蓝色框和黄色框标注了两个请求,我们主要看参数 noBlocking: true/false 的不同, 第一个是非阻塞方式请求, 可以看到共耗时6s,第二个共耗时12s! (第三个为啥和第二个耗时不一样——6s这个留给大家去研究) 。显而易见得非阻塞IO的优势。不过这代码结构就不那么友好了,看到代码 nio_server.php 中,有两种请求方式,阻塞代码流程还能看懂检查完成后就综合结果返回,而非阻塞方式中,发起三个检查后程序流程就开始进入到 handleMessage ,代码进入哪个分支,取决于 socket_read 的消息,不运行起程序来,没有文档,很难搞懂整个程序流程。

那么,有没有什么什么方便的php类库,让我们编码更友好一点呢,这里介绍下 ReactPHP

这里我用ReactPHP重新实现 nio_server , 代码在 这里

这个回调代码写起来有点 NodeJS 的味道呢,当你的PHP没启用 libev 之类的拓展时, ReactPHP 内部Loop依然用的 stream_select() , 可以看代码 ~/react/event-loop/src/StreamSelectLoop.php@290 .

执行效果如下:

r2yIfqV.png!web

这个同时发起请求这个业务逻辑,就还得提一下 curl_multi , 它能同时发起多个 curl 请求,最后不断检查是否所有的curl请求已完成。这只是在发起多个 curl 请求阶段做到 非阻塞 运行。

还有个拓展 pThreads ,能够实现多线程,不过对PHP编译参数有限制,需要在线程安全的模式下运行。

pThreads 现在已不是PHP官方所推荐使用的拓展了,当然了这种就属于 伪异步IO 范畴了

PHP 与 AIO

PHP 异步&非阻塞 编码。

此处, 非阻塞I/O 系统调用( nonblocking system call ) 和 异步I/O系统调用 (asychronous system call) 的区别是:

read()
read()
<?php

/**
 * 消息处理
 */
function handleMessage() {
    global $changed, $clients, $cartCheck;
    foreach ($changed as $key => $client) {
        while (true) {
            // read socket data
            $msg = @fread($client, 1024);
//            $msg = 1;
            if ($msg) {
                // application process
            } else {
                if (feof($client)) {
                    // TODO check data eof
                }
                break;
            }

可以看到,在文件 ~/nio_server.php 中, 虽然设置了 stream_set_blocking false , 但是在209行的 fread() , 这是在一个循环里读,这是一个阻塞读取。这的系统函数的响应速度是受系统IO影响的。

而异步调用中,当有 I/O事件 时,系统会将数据复制到用户内存中,也就是准备好数据,再通知到用户程序。

那么原生PHP显然是不支持的,这里呢就要引入PHP拓展,就是 Event ,或者 Ev 拓展。这篇博客主要讲 Event

Event 拓展是基于 libevent 库封装而来,而 Ev 拓展是基于 libev 库封装而来。 通过PHP接口,和C库的接口就能看到他们之间的联系,所以,如果通过PHP文档找不到相关资料可以去,看看C库的文档。

这里放上用 Event 实现的 Tcp Server demo

在用 Event 做这个 demo 中,我用到了 EventBuffer ,读、写都和 Buffer 交互, Buffer 数据是用户态数据,不会等待系统I/O或被阻塞,避免了程序耗时在I/O数据拷贝上。由此PHP 也能实现 AIO 程式,提高CPU利用率。

讲到这里,就会感觉这个PHP的 AIO 有些牵强了,我这找了其他博主的论点来帮助大家理解,先放两张图。

eqmYBv3.png!web

上面是 非阻塞IO ,下面是 异步IO 。中间的区别就是 非阻塞IO 的应用,需要不断的去访问内核获取数据(当然了,每一次访问都是有求必应,能取到数据),但不一定能取完; 而 异步IO 的特点就是,你告诉内核取数据,取完整了,我再一起发给应用程序。这就是Linux对 异步IO 的定义。

m2IrUvv.png!web

那么再看到我们的Demo,这是一个简单TCP server,一个TCP请求系统是能知道一个数据的包大小的,是否接收完毕,这是传输层要做的。而我们的应用层面,是接收到数据还要做合并,分包,以及数据转码。 这就和 AIO 数据结果必须是完整的 ,概率有些出入,(在系统层面显然是完整的) . 在应用层面呢,一次性收到的不一定是完整的数据,那么就还需要做额外代码来解决合包,分包,沾包。这就是 AIO 实现 Tcp Server 的需要问题。

为了解决以上问题,就需要自定义TCP通讯协议。相当于自己开发RPC框架了。

那我们来看看Http呢,在应用层面有明确公开的协议(协议有头无尾,标明了每次请求具体长度),并有丰富的实现。这就是一个非常适合采用 AIO 编程协议。而PHP的 Event 拓展,恰好有 EventHttp 实现。

话不多说,先上 Demo

<?php
...

/**
 * event http 请求回调函数
 * 
 * @param   \EventHttpRequest   $req    Http请求对象
 */
function _http_about($req) {
    echo __METHOD__, PHP_EOL;
    // print request URL
    echo "URI: ", $req->getUri(), PHP_EOL;
    // print request's headers
    echo "Input headers:"; var_dump($req->getInputHeaders());
    echo "\n >> Sending reply ...";
    /**
     * @var \EventBuffer    $buf
     */
    $buf = $req->getOutputBuffer();
    $buf->add("It's about Event http server");
    $req->sendReply(200, "OK", $buf);
    echo "OK\n";
}

这里是一个回调函数,入参数就是一个由 EventHttp 封装的http请求对象。这就满足了以上 调用时非阻塞,数据完全准备好后,再通知回调—— 异步I/O 。好,借助 Event ,PHP就实现了 AIO .

结语

关于性能提升,这就不做压测了,主要论证PHP实现 NIOAIO 的可行性。也实际给大家展示了几个 Demo , 简单展示了如何写 异步 , 非阻塞 程序。

以上,希望大家通过文章能了解 异步/同步阻塞/非阻塞 区别,以及对PHP 异步非阻塞 编程。

有问题欢迎提问~

参考

  1. PHP实现非阻塞
  2. PHP回顾之socket编程
  3. Cooperative multitasking using coroutines (in PHP!)
  4. IO - 同步,异步,阻塞,非阻塞
  5. 同步/异步,阻塞/非阻塞概念深度解析
  6. PHP之高性能I/O框架:Libevent
  7. 网络编程(三):从libevent到事件通知机制

About Joyk


Aggregate valuable and interesting links.
Joyk means Joy of geeK