3

I/O多路复用

IO多路复用就是通过一种机制,一个进程可以监听多个文件描述符,一个某个描述符就绪(一般是读就绪或写就绪),就能够通知程序进行相应的读写操作。select、poll、epoll本质上都是同步IO,因为他们需要在读写事件就绪后自己负责读写,即这个读写过程是阻塞的,而异步IO则无需自己负责读写,异步IO的实现会把数据从内核拷贝到用户空间。

select

基本原理

select 函数监视的文件描述符分3类,分别是writefds、readfds、和exceptfds。调用后select函数会阻塞,直到有描述符就绪(有数据 可读、可写、或者有except),或者超时(timeout指定等待时间,如果立即返回设为null即可),函数返回。当select函数返回后,可以通过遍历fdset,来找到就绪的描述符。

select的缺点

  • 单进程所能打开的文件描述符有一定限制,32位机默认1028,64位机默认2048。
  • 对socket进程扫描时是线性扫描,效率很低。
  • 用来存放文件描述符的数据结构,在用户空间和内核空间的复制开销极大。

poll

poll与select类似,略过。

epoll

epoll是在linux 2.6内核中提出的,是select和poll的增强版本。

基本原理

epoll支持水平触发和边缘触发,最大的特点在于边缘触发,它只告诉进程哪些fd刚刚变为就绪态,并且只会通知一次。还有一个特点是,epoll使用“事件”的就绪通知方式,一旦该fd就绪,内核就会采用类似callback的回调机制来激活该fd。

epoll的优点

  • 没有最大连接数的限制,1G内存约能监听10W个端口。
  • 不采用轮询的方式,不会随着FD数目的增加效率下降。只有活跃可用的FD才会回调。
  • 内存拷贝,epoll使用mmap减少复制开销。(注:mmap本质就是绕过从网卡、磁盘拷贝数据到内核再拷贝到用户空间的方式,直接从网卡拷贝数据到用户空间,性能爆炸。)

Thrift网络服务模型

thrift提供的网络服务模型有阻塞服务模型、非阻塞服务模型:

  • 阻塞服务模型:TSimpleServer、TThreadPoolServer
  • 非阻塞服务模型:TNonblockingServer、THsHaServer和TThreadedSelectorServer

TSimpleServer

该模式采用最简单的阻塞IO,一次只能接收并处理一个socket,处理流程如下:
alt text
此种模式效率低下,生产不会使用,略过。

TThreadPoolServer

TThreadPoolServer模式采用阻塞socket方式工作,主线程负责阻塞式(划重点,不是select的方式)监听是否有新socket到来,具体的业务处理交由一个线程池来处理。

accept部分的代码如下:

  protected TSocket acceptImpl() throws TTransportException {
    if (serverSocket_ == null) {
      throw new TTransportException(TTransportException.NOT_OPEN, "No underlying server socket.");
    }
    try {
      // 阻塞式监听新的连接
      Socket result = serverSocket_.accept();
      TSocket result2 = new TSocket(result);
      result2.setTimeout(clientTimeout_);
      return result2;
    } catch (IOException iox) {
      throw new TTransportException(iox);
    }
  }

具体模型如下:
alt text

TThreadPoolServer本质是One Thread Per Connection模型。模型受限于线程池的最大线程数,在连接数很大话,请求只能排队,对于高并发的场景,此模型并不合适。

TNonblockingServer

TNonblockingServer模式也是单线程工作,但是采用NIO的模式,借助Channel/Selector机制, 采用IO事件模型来处理。本质是一种event-loop模型。

具体模型如下:
alt text

event-loop的核心代码如下:

    private void select() {
      try {
        // 等待事件,jdk7之前的版本存在问题,会存在会将CPU打满的情况,没有事件,select却返回,从而将CPU打满;Netty中通过threshold,解决了该问题
        selector.select();

        // 获取IO事件
        Iterator<SelectionKey> selectedKeys = selector.selectedKeys().iterator();
        while (!stopped_ && selectedKeys.hasNext()) {
          SelectionKey key = selectedKeys.next();
          selectedKeys.remove();

          // skip if not valid
          if (!key.isValid()) {
            cleanupSelectionKey(key);
            continue;
          }

          // if the key is marked Accept, then it has to be the server
          // transport.
          // 处理连接事件
          if (key.isAcceptable()) {
            handleAccept();
          } else if (key.isReadable()) {
            // deal with reads
            // 处理读事件
            handleRead(key);
          } else if (key.isWritable()) {
            // deal with writes
            // 处理写事件
            handleWrite(key);
          } else {
            LOGGER.warn("Unexpected state in select! " + key.interestOps());
          }
        }
      } catch (IOException e) {
        LOGGER.warn("Got an IOException while selecting!", e);
      }
    }

这个模型一般由一个event dispatcher等待各类事件,待事件发生后原地调用对应的event handler,全部调用完后等待更多事件,故为"loop"。这个模型的实质是把多段逻辑按事件触发顺序交织在一个系统线程中。一个event-loop只能使用一个核,故此类程序要么是IO-bound,要么是每个handler有确定的较短的运行时间(比如http server),否则一个耗时漫长的回调就会卡住整个程序,产生高延时。在实践中这类程序不适合多开发者参与,一个人写了阻塞代码可能就会拖慢其他代码的响应。由于event handler不会同时运行,不太会产生复杂的race condition,一些代码不需要锁。此类程序主要靠部署更多进程增加扩展性。

THsHaServer

THsHaServer继承于TNonblockingServer,引入了线程池提高了任务处理的并发能力。THsHaServer是半同步半异步(Half-Sync/Half-Async)的处理模式,Half-Aysnc用于IO事件处理(Accept/Read/Write),Half-Sync用于业务handler对rpc的同步处理上。

具体模型如下:

alt text

THsHaServer与TNonblockingServer模式相比,THsHaServer在完成数据读取之后,将业务处理过程交由一个线程池来完成,主线程直接返回进行下一次循环操作,效率大大提升。

但是,主线程仍然需要处理accpet、read、write时间,当并发量非常大,读取或者发送的数据量比较大时,会将主线程阻塞住,新的连接无法被及时处理。

TThreadedSelectorServer

TThreadedSelectorServer是对THsHaServer的一种改进,它将selector中的read/write事件从主线程中剥离出来。

TThreadedSelectorServer是thrift提供的最高效的网络模型。具体模型如下:
alt text

构成如下:

  • 一个Accpet线程,专门用来处理新的socket
  • n个SelectorThread,用来处理read/write事件,读取、返回数据都是有这些线程完成的,每个SelectorThread会与若干个socket绑定,每个SelectorThread会处理与它绑定socket的read/write事件。
  • 一个负载均衡器SelectorThreadLoadBalancer对象,在accpet线程接收到新的socket以后,由SelectorThreadLoadBalancer决定将socket与哪个SelectorThread绑定(其实就是一个next函数,每分配一个socket,就调用next)。
  • 一个ExecutorService类型的worker线程池,在SelectorThread读取数据之后,将其包装成一个task,分配给worker线程池,处理业务逻辑。

总结:TThreadedSelectorServer模式,其实就是标准的Reactor模式,Tomcat7以后的版本、Cobar、MyCat(分库分表proxy)基本都是这个套路,具体实现略有差异。

原文链接

https://segmentfault.com/a/11...


扑火的蛾
272 声望30 粉丝

与世界分享你的装逼经验。