精华内容
下载资源
问答
  • BIO NIO

    2019-05-03 22:38:17
    本文从操作系统的角度来解释BIONIO,AIO的概念,含义和背后的那些。本文主要分为3篇。 第一篇 讲解BIONIO以及IO多路复用 第二篇 讲解磁盘IO和AIO 第三篇 讲解在这些机制上的一些应用的实现方式,比如nginx...

    本文从操作系统的角度来解释BIO,NIO,AIO的概念,含义和背后的那些事。本文主要分为3篇。

    • 第一篇 讲解BIO和NIO以及IO多路复用
    • 第二篇 讲解磁盘IO和AIO
    • 第三篇 讲解在这些机制上的一些应用的实现方式,比如nginx,nodejs,Java NIO等

    到底什么是“IO Block”

    很多人说BIO不好,会“block”,但到底什么是IO的Block呢?考虑下面两种情况:

    • 用系统调用read从socket里读取一段数据
    • 用系统调用read从一个磁盘文件读取一段数据到内存

    如果你的直觉告诉你,这两种都算“Block”,那么很遗憾,你的理解与Linux不同。Linux认为:

    • 对于第一种情况,算作block,因为Linux无法知道网络上对方是否会发数据。如果没数据发过来,对于调用read的程序来说,就只能“等”。

    • 对于第二种情况,不算做block

    是的,对于磁盘文件IO,Linux总是不视作Block。

    你可能会说,这不科学啊,磁盘读写偶尔也会因为硬件而卡壳啊,怎么能不算Block呢?但实际就是不算。

    一个解释是,所谓“Block”是指操作系统可以预见这个Block会发生才会主动Block。例如当读取TCP连接的数据时,如果发现Socket buffer里没有数据就可以确定定对方还没有发过来,于是Block;而对于普通磁盘文件的读写,也许磁盘运作期间会抖动,会短暂暂停,但是操作系统无法预见这种情况,只能视作不会Block,照样执行。

    基于这个基本的设定,在讨论IO时,一定要严格区分网络IO和磁盘文件IO。NIO和后文讲到的IO多路复用只对网络IO有意义。

    严格的说,O_NONBLOCK和IO多路复用,对标准输入输出描述符、管道和FIFO也都是有效的。但本文侧重于讨论高性能网络服务器下各种IO的含义和关系,所以本文做了简化,只提及网络IO和磁盘文件IO两种情况。

    本文先着重讲一下网络IO。

    BIO

    有了Block的定义,就可以讨论BIO和NIO了。BIO是Blocking IO的意思。在类似于网络中进行read, write, connect一类的系统调用时会被卡住。

    举个例子,当用read去读取网络的数据时,是无法预知对方是否已经发送数据的。因此在收到数据之前,能做的只有等待,直到对方把数据发过来,或者等到网络超时。

    对于单线程的网络服务,这样做就会有卡死的问题。因为当等待时,整个线程会被挂起,无法执行,也无法做其他的工作。

    顺便说一句,这种Block是不会影响同时运行的其他程序(进程)的,因为现代操作系统都是多任务的,任务之间的切换是抢占式的。这里Block只是指Block当前的进程。

    于是,网络服务为了同时响应多个并发的网络请求,必须实现为多线程的。每个线程处理一个网络请求。线程数随着并发连接数线性增长。这的确能奏效。实际上2000年之前很多网络服务器就是这么实现的。但这带来两个问题:

    • 线程越多,Context Switch就越多,而Context Switch是一个比较重的操作,会无谓浪费大量的CPU。
    • 每个线程会占用一定的内存作为线程的栈。比如有1000个线程同时运行,每个占用1MB内存,就占用了1个G的内存。

    也许现在看来1GB内存不算什么,现在服务器上百G内存的配置现在司空见惯了。但是倒退20年,1G内存是很金贵的。并且,尽管现在通过使用大内存,可以轻易实现并发1万甚至10万的连接。但是水涨船高,如果是要单机撑1千万的连接呢?

    问题的关键在于,当调用read接受网络请求时,有数据到了就用,没数据到时,实际上是可以干别的。使用大量线程,仅仅是因为Block发生,没有其他办法。

    当然你可能会说,是不是可以弄个线程池呢?这样既能并发的处理请求,又不会产生大量线程。但这样会限制最大并发的连接数。比如你弄4个线程,那么最大4个线程都Block了就没法响应更多请求了。

    要是操作IO接口时,操作系统能够总是直接告诉有没有数据,而不是Block去等就好了。于是,NIO登场。

    NIO

    NIO是指将IO模式设为“Non-Blocking”模式。在Linux下,一般是这样:

    void setnonblocking(int fd) {
        int flags = fcntl(fd, F_GETFL, 0);
        fcntl(fd, F_SETFL, flags | O_NONBLOCK);
    }
    

    再强调一下,以上操作只对socket对应的文件描述符有意义;对磁盘文件的文件描述符做此设置总会成功,但是会直接被忽略。

    这时,BIO和NIO的区别是什么呢?

    在BIO模式下,调用read,如果发现没数据已经到达,就会Block住。

    在NIO模式下,调用read,如果发现没数据已经到达,就会立刻返回-1, 并且errno被设为EAGAIN

    在有些文档中写的是会返回EWOULDBLOCK。实际上,在Linux下EAGAINEWOULDBLOCK是一样的,即#define EWOULDBLOCK EAGAIN

    于是,一段NIO的代码,大概就可以写成这个样子。

    struct timespec sleep_interval{.tv_sec = 0, .tv_nsec = 1000};
    ssize_t nbytes;
    while (1) {
        /* 尝试读取 */
        if ((nbytes = read(fd, buf, sizeof(buf))) < 0) {
            if (errno == EAGAIN) { // 没数据到
                perror("nothing can be read");
            } else {
                perror("fatal error");
                exit(EXIT_FAILURE);
            }
        } else { // 有数据
            process_data(buf, nbytes);
        }
        // 处理其他事情,做完了就等一会,再尝试
        nanosleep(sleep_interval, NULL);
    }
    

    这段代码很容易理解,就是轮询,不断的尝试有没有数据到达,有了就处理,没有(得到EWOULDBLOCK或者EAGAIN)就等一小会再试。这比之前BIO好多了,起码程序不会被卡死了。

    但这样会带来两个新问题:

    • 如果有大量文件描述符都要等,那么就得一个一个的read。这会带来大量的Context Switch(read是系统调用,每调用一次就得在用户态和核心态切换一次)
    • 休息一会的时间不好把握。这里是要猜多久之后数据才能到。等待时间设的太长,程序响应延迟就过大;设的太短,就会造成过于频繁的重试,干耗CPU而已。

    要是操作系统能一口气告诉程序,哪些数据到了就好了。

    于是IO多路复用被搞出来解决这个问题。

    IO多路复用

    IO多路复用(IO Multiplexing) 是这么一种机制:程序注册一组socket文件描述符给操作系统,表示“我要监视这些fd是否有IO事件发生,有了就告诉程序处理”。

    IO多路复用是要和NIO一起使用的。尽管在操作系统级别,NIO和IO多路复用是两个相对独立的事情。NIO仅仅是指IO API总是能立刻返回,不会被Blocking;而IO多路复用仅仅是操作系统提供的一种便利的通知机制。操作系统并不会强制这俩必须得一起用——你可以用NIO,但不用IO多路复用,就像上一节中的代码;也可以只用IO多路复用 + BIO,这时效果还是当前线程被卡住。但是,IO多路复用和NIO是要配合一起使用才有实际意义。因此,在使用IO多路复用之前,请总是先把fd设为O_NONBLOCK

    对IO多路复用,还存在一些常见的误解,比如:

    • ❌IO多路复用是指多个数据流共享同一个Socket。其实IO多路复用说的是多个Socket,只不过操作系统是一起监听他们的事件而已。

      多个数据流共享同一个TCP连接的场景的确是有,比如Http2 Multiplexing就是指Http2通讯中中多个逻辑的数据流共享同一个TCP连接。但这与IO多路复用是完全不同的问题。

    • ❌IO多路复用是NIO,所以总是不Block的。其实IO多路复用的关键API调用(selectpollepoll_wait)总是Block的,正如下文的例子所讲。

    • IO多路复用和NIO一起减少了IO。实际上,IO本身(网络数据的收发)无论用不用IO多路复用和NIO,都没有变化。请求的数据该是多少还是多少;网络上该传输多少数据还是多少数据。IO多路复用和NIO一起仅仅是解决了调度的问题,避免CPU在这个过程中的浪费,使系统的瓶颈更容易触达到网络带宽,而非CPU或者内存。要提高IO吞吐,还是提高硬件的容量(例如,用支持更大带宽的网线、网卡和交换机)和依靠并发传输(例如HDFS的数据多副本并发传输)。

    操作系统级别提供了一些接口来支持IO多路复用,最老掉牙的是selectpoll

    select

    select长这样:

    int select(int nfds, fd_set *readfds, fd_set *writefds, fd_set *exceptfds, struct timeval *timeout);
    

    它接受3个文件描述符的数组,分别监听读取(readfds),写入(writefds)和异常(expectfds)事件。那么一个 IO多路复用的代码大概是这样:

    struct timeval tv = {.tv_sec = 1, .tv_usec = 0};
    
    ssize_t nbytes;
    while(1) {
        FD_ZERO(&read_fds);
        setnonblocking(fd1);
        setnonblocking(fd2);
        FD_SET(fd1, &read_fds);
        FD_SET(fd2, &read_fds);
        // 把要监听的fd拼到一个数组里,而且每次循环都得重来一次...
        if (select(FD_SETSIZE, &read_fds, NULL, NULL, &tv) < 0) { // block住,直到有事件到达
            perror("select出错了");
            exit(EXIT_FAILURE);
        }
        for (int i = 0; i < FD_SETSIZE; i++) {
            if (FD_ISSET(i, &read_fds)) {
                /* 检测到第[i]个读取fd已经收到了,这里假设buf总是大于到达的数据,所以可以一次read完 */
                if ((nbytes = read(i, buf, sizeof(buf))) >= 0) {
                    process_data(nbytes, buf);
                } else {
                    perror("读取出错了");
                    exit(EXIT_FAILURE);
                }
            }
        }
    }
    

    首先,为了select需要构造一个fd数组(这里为了简化,没有构造要监听写入和异常事件的fd数组)。之后,用select监听了read_fds中的多个socket的读取时间。调用select后,程序会Block住,直到一个事件发生了,或者等到最大1秒钟(tv定义了这个时间长度)就返回。之后,需要遍历所有注册的fd,挨个检查哪个fd有事件到达(FD_ISSET返回true)。如果是,就说明数据已经到达了,可以读取fd了。读取后就可以进行数据的处理。

    select有一些发指的缺点:

    • select能够支持的最大的fd数组的长度是1024。这对要处理高并发的web服务器是不可接受的。
    • fd数组按照监听的事件分为了3个数组,为了这3个数组要分配3段内存去构造,而且每次调用select前都要重设它们(因为select会改这3个数组);调用select后,这3数组要从用户态复制一份到内核态;事件到达后,要遍历这3数组。很不爽。
    • select返回后要挨个遍历fd,找到被“SET”的那些进行处理。这样比较低效。
    • select是无状态的,即每次调用select,内核都要重新检查所有被注册的fd的状态。select返回后,这些状态就被返回了,内核不会记住它们;到了下一次调用,内核依然要重新检查一遍。于是查询的效率很低。

    poll

    pollselect类似于。它大概长这样:

    int poll(struct pollfd *fds, nfds_t nfds, int timeout);
    

    poll的代码例子和select差不多,因此也就不赘述了。有意思的是poll这个单词的意思是“轮询”,所以很多中文资料都会提到对IO进行“轮询”。

    上面说的select和下文说的epoll本质上都是轮询。

    poll优化了select的一些问题。比如不再有3个数组,而是1个polldfd结构的数组了,并且也不需要每次重设了。数组的个数也没有了1024的限制。但其他的问题依旧:

    • 依然是无状态的,性能的问题与select差不多一样;
    • 应用程序仍然无法很方便的拿到那些“有事件发生的fd“,还是需要遍历所有注册的fd。

    目前来看,高性能的web服务器都不会使用selectpoll。他们俩存在的意义仅仅是“兼容性”,因为很多操作系统都实现了这两个系统调用。

    如果是追求性能的话,在BSD/macOS上提供了kqueue api;在Salorias中提供了/dev/poll(可惜该操作系统已经凉凉);而在Linux上提供了epoll api。它们的出现彻底解决了selectpoll的问题。Java NIO,nginx等在对应的平台的上都是使用这些api实现。

    因为大部分情况下我会用Linux做服务器,所以下文以Linux epoll为例子来解释多路复用是怎么工作的。

    用epoll实现的IO多路复用

    epoll是Linux下的IO多路复用的实现。这里单开一章是因为它非常有代表性,并且Linux也是目前最广泛被作为服务器的操作系统。细致的了解epoll对整个IO多路复用的工作原理非常有帮助。

    selectpoll不同,要使用epoll是需要先创建一下的。

    int epfd = epoll_create(10);
    

    epoll_create在内核层创建了一个数据表,接口会返回一个“epoll的文件描述符”指向这个表。注意,接口参数是一个表达要监听事件列表的长度的数值。但不用太在意,因为epoll内部随后会根据事件注册和事件注销动态调整epoll中表格的大小。

    epoll创建

    为什么epoll要创建一个用文件描述符来指向的表呢?这里有两个好处:

    • epoll是有状态的,不像selectpoll那样每次都要重新传入所有要监听的fd,这避免了很多无谓的数据复制。epoll的数据是用接口epoll_ctl来管理的(增、删、改)。
    • epoll文件描述符在进程被fork时,子进程是可以继承的。这可以给对多进程共享一份epoll数据,实现并行监听网络请求带来便利。但这超过了本文的讨论范围,就此打住。

    epoll创建后,第二步是使用epoll_ctl接口来注册要监听的事件。

    int epoll_ctl(int epfd, int op, int fd, struct epoll_event *event);
    

    其中第一个参数就是上面创建的epfd。第二个参数op表示如何对文件名进行操作,共有3种。

    • EPOLL_CTL_ADD - 注册一个事件
    • EPOLL_CTL_DEL - 取消一个事件的注册
    • EPOLL_CTL_MOD - 修改一个事件的注册

    第三个参数是要操作的fd,这里必须是支持NIO的fd(比如socket)。

    第四个参数是一个epoll_event的类型的数据,表达了注册的事件的具体信息。

    typedef union epoll_data {
        void    *ptr;
        int      fd;
        uint32_t u32;
        uint64_t u64;
    } epoll_data_t;
    
    struct epoll_event {
        uint32_t     events;    /* Epoll events */
        epoll_data_t data;      /* User data variable */
    };
    

    比方说,想关注一个fd1的读取事件事件,并采用边缘触发(下文会解释什么是边缘触发),大概要这么写:

    struct epoll_data ev;
    ev.events = EPOLLIN | EPOLLET; // EPOLLIN表示读事件;EPOLLET表示边缘触发
    ev.data.fd = fd1;
    

    通过epoll_ctl就可以灵活的注册/取消注册/修改注册某个fd的某些事件。

    管理fd事件注册

    第三步,使用epoll_wait来等待事件的发生。

    int epoll_wait(int epfd, struct epoll_event *evlist, int maxevents, int timeout);
    

    特别留意,这一步是"block"的。只有当注册的事件至少有一个发生,或者timeout达到时,该调用才会返回。这与selectpoll几乎一致。但不一样的地方是evlist,它是epoll_wait的返回数组,里面只包含那些被触发的事件对应的fd,而不是像selectpoll那样返回所有注册的fd。

    监听fd事件

    综合起来,一段比较完整的epoll代码大概是这样的。

    #define MAX_EVENTS 10
    struct epoll_event ev, events[MAX_EVENTS];
    int nfds, epfd, fd1, fd2;
    
    // 假设这里有两个socket,fd1和fd2,被初始化好。
    // 设置为non blocking
    setnonblocking(fd1);
    setnonblocking(fd2);
    
    // 创建epoll
    epfd = epoll_create(MAX_EVENTS);
    if (epollfd == -1) {
        perror("epoll_create1");
        exit(EXIT_FAILURE);
    }
    
    //注册事件
    ev.events = EPOLLIN | EPOLLET;
    ev.data.fd = fd1;
    if (epoll_ctl(epollfd, EPOLL_CTL_ADD, fd1, &ev) == -1) {
        perror("epoll_ctl: error register fd1");
        exit(EXIT_FAILURE);
    }
    if (epoll_ctl(epollfd, EPOLL_CTL_ADD, fd2, &ev) == -1) {
        perror("epoll_ctl: error register fd2");
        exit(EXIT_FAILURE);
    }
    
    // 监听事件
    for (;;) {
        nfds = epoll_wait(epdf, events, MAX_EVENTS, -1);
        if (nfds == -1) {
            perror("epoll_wait");
            exit(EXIT_FAILURE);
        }
    
        for (n = 0; n < nfds; ++n) { // 处理所有发生IO事件的fd
            process_event(events[n].data.fd);
            // 如果有必要,可以利用epoll_ctl继续对本fd注册下一次监听,然后重新epoll_wait
        }
    }
    

    此外,epoll的手册 中也有一个简单的例子。

    所有的基于IO多路复用的代码都会遵循这样的写法:注册——监听事件——处理——再注册,无限循环下去。

    epoll的优势

    为什么epoll的性能比selectpoll要强呢? selectpoll每次都需要把完成的fd列表传入到内核,迫使内核每次必须从头扫描到尾。而epoll完全是反过来的。epoll在内核的数据被建立好了之后,每次某个被监听的fd一旦有事件发生,内核就直接标记之。epoll_wait调用时,会尝试直接读取到当时已经标记好的fd列表,如果没有就会进入等待状态。

    同时,epoll_wait直接只返回了被触发的fd列表,这样上层应用写起来也轻松愉快,再也不用从大量注册的fd中筛选出有事件的fd了。

    简单说就是selectpoll的代价是"O(所有注册事件fd的数量)",而epoll的代价是"O(发生事件fd的数量)"。于是,高性能网络服务器的场景特别适合用epoll来实现——因为大多数网络服务器都有这样的模式:同时要监听大量(几千,几万,几十万甚至更多)的网络连接,但是短时间内发生的事件非常少。

    但是,假设发生事件的fd的数量接近所有注册事件fd的数量,那么epoll的优势就没有了,其性能表现会和pollselect差不多。

    epoll除了性能优势,还有一个优点——同时支持水平触发(Level Trigger)和边沿触发(Edge Trigger)。

    水平触发和边沿触发

    默认情况下,epoll使用水平触发,这与selectpoll的行为完全一致。在水平触发下,epoll顶多算是一个“跑得更快的poll”。

    而一旦在注册事件时使用了EPOLLET标记(如上文中的例子),那么将其视为边沿触发(或者有地方叫边缘触发,一个意思)。那么到底什么水平触发和边沿触发呢?

    考虑下图中的例子。有两个socket的fd——fd1和fd2。我们设定监听f1的“水平触发读事件“,监听fd2的”边沿触发读事件“。我们使用在时刻t1,使用epoll_wait监听他们的事件。在时刻t2时,两个fd都到了100bytes数据,于是在时刻t3, epoll_wait返回了两个fd进行处理。在t4,我们故意不读取所有的数据出来,只各自读50bytes。然后在t5重新注册两个事件并监听。在t6时,只有fd1会返回,因为fd1里的数据没有读完,仍然处于“被触发”状态;而fd2不会被返回,因为没有新数据到达。

    水平触发和边沿触发

    这个例子很明确的显示了水平触发和边沿触发的区别。

    • 水平触发只关心文件描述符中是否还有没完成处理的数据,如果有,不管怎样epoll_wait,总是会被返回。简单说——水平触发代表了一种“状态”。

    • 边沿触发只关心文件描述符是否有的事件产生,如果有,则返回;如果返回过一次,不管程序是否处理了,只要没有新的事件产生,epoll_wait不会再认为这个fd被“触发”了。简单说——边沿触发代表了一个“事件”。

      那么边沿触发怎么才能迫使新事件产生呢?一般需要反复调用read/write这样的IO接口,直到得到了EAGAIN错误码,再去尝试epoll_wait才有可能得到下次事件。

    那么为什么需要边沿触发呢?

    边沿触发把如何处理数据的控制权完全交给了开发者,提供了巨大的灵活性。比如,读取一个http的请求,开发者可以决定只读取http中的headers数据就停下来,然后根据业务逻辑判断是否要继续读(比如需要调用另外一个服务来决定是否继续读)。而不是次次被socket尚有数据的状态烦扰;写入数据时也是如此。比如希望将一个资源A写入到socket。当socket的buffer充足时,epoll_wait会返回这个fd是准备好的。但是资源A此时不一定准备好。如果使用水平触发,每次经过epoll_wait也总会被打扰。在边沿触发下,开发者有机会更精细的定制这里的控制逻辑。

    但不好的一面时,边沿触发也大大的提高了编程的难度。一不留神,可能就会miss掉处理部分socket数据的机会。如果没有很好的根据EAGAIN来“重置”一个fd,就会造成此fd永远没有新事件产生,进而导致饿死相关的处理代码。

    再来思考一下什么是“Block”

    上面的所有介绍都在围绕如何让网络IO不会被Block。但是网络IO处理仅仅是整个数据处理中的一部分。如果你留意到上文例子中的“处理事件”代码,就会发现这里可能是有问题的。

    • 处理代码有可能需要读写文件,可能会很慢,从而干扰整个程序的效率;
    • 处理代码有可能是一段复杂的数据计算,计算量很大的话,就会卡住整个执行流程;
    • 处理代码有bug,可能直接进入了一段死循环……

    这时你会发现,这里的Block和本文之初讲的O_NONBLOCK是不同的事情。在一个网络服务中,如果处理程序的延迟远远小于网络IO,那么这完全不成问题。但是如果处理程序的延迟已经大到无法忽略了,就会对整个程序产生很大的影响。这时IO多路复用已经不是问题的关键。

    试分析和比较下面两个场景:

    • web proxy。程序通过IO多路复用接收到了请求之后,直接转发给另外一个网络服务。
    • web server。程序通过IO多路复用接收到了请求之后,需要读取一个文件,并返回其内容。

    它们有什么不同?它们的瓶颈可能出在哪里?

    总结

    小结一下本文:

    • 对于socket的文件描述符才有所谓BIO和NIO。
    • 多线程+BIO模式会带来大量的资源浪费,而NIO+IO多路复用可以解决这个问题。
    • 在Linux下,基于epoll的IO多路复用是解决这个问题的最佳方案;epoll相比selectpoll有很大的性能优势和功能优势,适合实现高性能网络服务。

    但是IO多路复用仅仅是解决了一部分问题,另外一部分问题如何解决呢?且听下回分解。



     

    展开全文
  • BIO NIO解析

    千次阅读 2018-05-03 17:22:17
    文章出処:https://zhuanlan.zhihu.com/p/23488863NIO(Non-blocking I/O,在Java领域,也称为New I/O),是一种同步非...那么NIO的本质是什么样的呢?它是怎样与事件模型结合来解放线程、提高系统吞吐的呢?本文会...


    文章出処:https://zhuanlan.zhihu.com/p/23488863

    NIO(Non-blocking I/O,在Java领域,也称为New I/O),是一种同步非阻塞的I/O模型,也是I/O多路复用的基础,已经被越来越多地应用到大型应用服务器,成为解决高并发与大量连接、I/O处理问题的有效方式。

    那么NIO的本质是什么样的呢?它是怎样与事件模型结合来解放线程、提高系统吞吐的呢?

    本文会从传统的阻塞I/O和线程池模型面临的问题讲起,然后对比几种常见I/O模型,一步步分析NIO怎么利用事件模型处理I/O,解决线程池瓶颈处理海量连接,包括利用面向事件的方式编写服务端/客户端程序。最后延展到一些高级主题,如Reactor与Proactor模型的对比、Selector的唤醒、Buffer的选择等。

    注:本文的代码都是伪代码,主要是为了示意,不可用于生产环境。

    传统BIO模型分析

    让我们先回忆一下传统的服务器端同步阻塞I/O处理(也就是BIO,Blocking I/O)的经典编程模型:

    {
     ExecutorService executor = Excutors.newFixedThreadPollExecutor(100);//线程池
    
     ServerSocket serverSocket = new ServerSocket();
     serverSocket.bind(8088);
     while(!Thread.currentThread.isInturrupted()){//主线程死循环等待新连接到来
     Socket socket = serverSocket.accept();
     executor.submit(new ConnectIOnHandler(socket));//为新的连接创建新的线程
    }
    
    class ConnectIOnHandler extends Thread{
        private Socket socket;
        public ConnectIOnHandler(Socket socket){
           this.socket = socket;
        }
        public void run(){
          while(!Thread.currentThread.isInturrupted()&&!socket.isClosed()){死循环处理读写事件
              String someThing = socket.read()....//读取数据
              if(someThing!=null){
                 ......//处理数据
                 socket.write()....//写数据
              }
    
          }
        }
    }
    

    这是一个经典的每连接每线程的模型,之所以使用多线程,主要原因在于socket.accept()、socket.read()、socket.write()三个主要函数都是同步阻塞的,当一个连接在处理I/O的时候,系统是阻塞的,如果是单线程的话必然就挂死在那里;但CPU是被释放出来的,开启多线程,就可以让CPU去处理更多的事情。其实这也是所有使用多线程的本质:

    1. 利用多核。
    2. 当I/O阻塞系统,但CPU空闲的时候,可以利用多线程使用CPU资源。

    现在的多线程一般都使用线程池,可以让线程的创建和回收成本相对较低。在活动连接数不是特别高(小于单机1000)的情况下,这种模型是比较不错的,可以让每一个连接专注于自己的I/O并且编程模型简单,也不用过多考虑系统的过载、限流等问题。线程池本身就是一个天然的漏斗,可以缓冲一些系统处理不了的连接或请求。

    不过,这个模型最本质的问题在于,严重依赖于线程。但线程是很"贵"的资源,主要表现在:

    1. 线程的创建和销毁成本很高,在Linux这样的操作系统中,线程本质上就是一个进程。创建和销毁都是重量级的系统函数。
    2. 线程本身占用较大内存,像Java的线程栈,一般至少分配512K~1M的空间,如果系统中的线程数过千,恐怕整个JVM的内存都会被吃掉一半。
    3. 线程的切换成本是很高的。操作系统发生线程切换的时候,需要保留线程的上下文,然后执行系统调用。如果线程数过高,可能执行线程切换的时间甚至会大于线程执行的时间,这时候带来的表现往往是系统load偏高、CPU sy使用率特别高(超过20%以上),导致系统几乎陷入不可用的状态。
    4. 容易造成锯齿状的系统负载。因为系统负载是用活动线程数或CPU核心数,一旦线程数量高但外部网络环境不是很稳定,就很容易造成大量请求的结果同时返回,激活大量阻塞线程从而使系统负载压力过大。

    所以,当面对十万甚至百万级连接的时候,传统的BIO模型是无能为力的。随着移动端应用的兴起和各种网络游戏的盛行,百万级长连接日趋普遍,此时,必然需要一种更高效的I/O处理模型。

    NIO是怎么工作的

    很多刚接触NIO的人,第一眼看到的就是Java相对晦涩的API,比如:Channel,Selector,Socket什么的;然后就是一坨上百行的代码来演示NIO的服务端Demo……瞬间头大有没有?

    我们不管这些,抛开现象看本质,先分析下NIO是怎么工作的。

    常见I/O模型对比

    所有的系统I/O都分为两个阶段:等待就绪和操作。举例来说,读函数,分为等待系统可读和真正的读;同理,写函数分为等待网卡可以写和真正的写。

    需要说明的是等待就绪的阻塞是不使用CPU的,是在“空等”;而真正的读写操作的阻塞是使用CPU的,真正在"干活",而且这个过程非常快,属于memory copy,带宽通常在1GB/s级别以上,可以理解为基本不耗时。

    下图是几种常见I/O模型的对比:


    以socket.read()为例子:

    传统的BIO里面socket.read(),如果TCP RecvBuffer里没有数据,函数会一直阻塞,直到收到数据,返回读到的数据。

    对于NIO,如果TCP RecvBuffer有数据,就把数据从网卡读到内存,并且返回给用户;反之则直接返回0,永远不会阻塞。

    最新的AIO(Async I/O)里面会更进一步:不但等待就绪是非阻塞的,就连数据从网卡到内存的过程也是异步的。

    换句话说,BIO里用户最关心“我要读”,NIO里用户最关心"我可以读了",在AIO模型里用户更需要关注的是“读完了”。

    NIO一个重要的特点是:socket主要的读、写、注册和接收函数,在等待就绪阶段都是非阻塞的,真正的I/O操作是同步阻塞的(消耗CPU但性能非常高)。

    如何结合事件模型使用NIO同步非阻塞特性

    回忆BIO模型,之所以需要多线程,是因为在进行I/O操作的时候,一是没有办法知道到底能不能写、能不能读,只能"傻等",即使通过各种估算,算出来操作系统没有能力进行读写,也没法在socket.read()和socket.write()函数中返回,这两个函数无法进行有效的中断。所以除了多开线程另起炉灶,没有好的办法利用CPU。

    NIO的读写函数可以立刻返回,这就给了我们不开线程利用CPU的最好机会:如果一个连接不能读写(socket.read()返回0或者socket.write()返回0),我们可以把这件事记下来,记录的方式通常是在Selector上注册标记位,然后切换到其它就绪的连接(channel)继续进行读写。

    下面具体看下如何利用事件模型单线程处理所有I/O请求:

    NIO的主要事件有几个:读就绪、写就绪、有新连接到来。

    我们首先需要注册当这几个事件到来的时候所对应的处理器。然后在合适的时机告诉事件选择器:我对这个事件感兴趣。对于写操作,就是写不出去的时候对写事件感兴趣;对于读操作,就是完成连接和系统没有办法承载新读入的数据的时;对于accept,一般是服务器刚启动的时候;而对于connect,一般是connect失败需要重连或者直接异步调用connect的时候。

    其次,用一个死循环选择就绪的事件,会执行系统调用(Linux 2.6之前是select、poll,2.6之后是epoll,Windows是IOCP),还会阻塞的等待新事件的到来。新事件到来的时候,会在selector上注册标记位,标示可读、可写或者有连接到来。

    注意,select是阻塞的,无论是通过操作系统的通知(epoll)还是不停的轮询(select,poll),这个函数是阻塞的。所以你可以放心大胆地在一个while(true)里面调用这个函数而不用担心CPU空转。

    所以我们的程序大概的模样是:

       interface ChannelHandler{
          void channelReadable(Channel channel);
          void channelWritable(Channel channel);
       }
       class Channel{
         Socket socket;
         Event event;//读,写或者连接
       }
    
       //IO线程主循环:
       class IoThread extends Thread{
       public void run(){
       Channel channel;
       while(channel=Selector.select()){//选择就绪的事件和对应的连接
          if(channel.event==accept){
             registerNewChannelHandler(channel);//如果是新连接,则注册一个新的读写处理器
          }
          if(channel.event==write){
             getChannelHandler(channel).channelWritable(channel);//如果可以写,则执行写事件
          }
          if(channel.event==read){
              getChannelHandler(channel).channelReadable(channel);//如果可以读,则执行读事件
          }
        }
       }
       Map<Channel,ChannelHandler> handlerMap;//所有channel的对应事件处理器
      }
    

    这个程序很简短,也是最简单的Reactor模式:注册所有感兴趣的事件处理器,单线程轮询选择就绪事件,执行事件处理器。

    优化线程模型

    由上面的示例我们大概可以总结出NIO是怎么解决掉线程的瓶颈并处理海量连接的:

    NIO由原来的阻塞读写(占用线程)变成了单线程轮询事件,找到可以进行读写的网络描述符进行读写。除了事件的轮询是阻塞的(没有可干的事情必须要阻塞),剩余的I/O操作都是纯CPU操作,没有必要开启多线程。

    并且由于线程的节约,连接数大的时候因为线程切换带来的问题也随之解决,进而为处理海量连接提供了可能。

    单线程处理I/O的效率确实非常高,没有线程切换,只是拼命的读、写、选择事件。但现在的服务器,一般都是多核处理器,如果能够利用多核心进行I/O,无疑对效率会有更大的提高。

    仔细分析一下我们需要的线程,其实主要包括以下几种:

    1. 事件分发器,单线程选择就绪的事件。
    2. I/O处理器,包括connect、read、write等,这种纯CPU操作,一般开启CPU核心个线程就可以。
    3. 业务线程,在处理完I/O后,业务一般还会有自己的业务逻辑,有的还会有其他的阻塞I/O,如DB操作,RPC等。只要有阻塞,就需要单独的线程。

    Java的Selector对于Linux系统来说,有一个致命限制:同一个channel的select不能被并发的调用。因此,如果有多个I/O线程,必须保证:一个socket只能属于一个IoThread,而一个IoThread可以管理多个socket。

    另外连接的处理和读写的处理通常可以选择分开,这样对于海量连接的注册和读写就可以分发。虽然read()和write()是比较高效无阻塞的函数,但毕竟会占用CPU,如果面对更高的并发则无能为力。

    NIO在客户端的魔力

    通过上面的分析,可以看出NIO在服务端对于解放线程,优化I/O和处理海量连接方面,确实有自己的用武之地。那么在客户端上,NIO又有什么使用场景呢?

    常见的客户端BIO+连接池模型,可以建立n个连接,然后当某一个连接被I/O占用的时候,可以使用其他连接来提高性能。

    但多线程的模型面临和服务端相同的问题:如果指望增加连接数来提高性能,则连接数又受制于线程数、线程很贵、无法建立很多线程,则性能遇到瓶颈。

    每连接顺序请求的Redis

    对于Redis来说,由于服务端是全局串行的,能够保证同一连接的所有请求与返回顺序一致。这样可以使用单线程+队列,把请求数据缓冲。然后pipeline发送,返回future,然后channel可读时,直接在队列中把future取回来,done()就可以了。

    伪代码如下:

    class RedisClient Implements ChannelHandler{
     private BlockingQueue CmdQueue;
     private EventLoop eventLoop;
     private Channel channel;
     class Cmd{
      String cmd;
      Future result;
     }
     public Future get(String key){
       Cmd cmd= new Cmd(key);
       queue.offer(cmd);
       eventLoop.submit(new Runnable(){
            List list = new ArrayList();
            queue.drainTo(list);
            if(channel.isWritable()){
             channel.writeAndFlush(list);
            }
       });
    }
     public void ChannelReadFinish(Channel channel,Buffer Buffer){
        List result = handleBuffer();//处理数据
        //从cmdQueue取出future,并设值,future.done();
    }
     public void ChannelWritable(Channel channel){
       channel.flush();
    }
    }
    

    这样做,能够充分的利用pipeline来提高I/O能力,同时获取异步处理能力。

    多连接短连接的HttpClient

    类似于竞对抓取的项目,往往需要建立无数的HTTP短连接,然后抓取,然后销毁,当需要单机抓取上千网站线程数又受制的时候,怎么保证性能呢?

    何不尝试NIO,单线程进行连接、写、读操作?如果连接、读、写操作系统没有能力处理,简单的注册一个事件,等待下次循环就好了。

    如何存储不同的请求/响应呢?由于http是无状态没有版本的协议,又没有办法使用队列,好像办法不多。比较笨的办法是对于不同的socket,直接存储socket的引用作为map的key。

    常见的RPC框架,如Thrift,Dubbo

    这种框架内部一般维护了请求的协议和请求号,可以维护一个以请求号为key,结果的result为future的map,结合NIO+长连接,获取非常不错的性能。

    NIO高级主题

    Proactor与Reactor

    一般情况下,I/O 复用机制需要事件分发器(event dispatcher)。 事件分发器的作用,即将那些读写事件源分发给各读写事件的处理者,就像送快递的在楼下喊: 谁谁谁的快递到了, 快来拿吧!开发人员在开始的时候需要在分发器那里注册感兴趣的事件,并提供相应的处理者(event handler),或者是回调函数;事件分发器在适当的时候,会将请求的事件分发给这些handler或者回调函数。

    涉及到事件分发器的两种模式称为:Reactor和Proactor。 Reactor模式是基于同步I/O的,而Proactor模式是和异步I/O相关的。在Reactor模式中,事件分发器等待某个事件或者可应用或个操作的状态发生(比如文件描述符可读写,或者是socket可读写),事件分发器就把这个事件传给事先注册的事件处理函数或者回调函数,由后者来做实际的读写操作。

    而在Proactor模式中,事件处理者(或者代由事件分发器发起)直接发起一个异步读写操作(相当于请求),而实际的工作是由操作系统来完成的。发起时,需要提供的参数包括用于存放读到数据的缓存区、读的数据大小或用于存放外发数据的缓存区,以及这个请求完后的回调函数等信息。事件分发器得知了这个请求,它默默等待这个请求的完成,然后转发完成事件给相应的事件处理者或者回调。举例来说,在Windows上事件处理者投递了一个异步IO操作(称为overlapped技术),事件分发器等IO Complete事件完成。这种异步模式的典型实现是基于操作系统底层异步API的,所以我们可称之为“系统级别”的或者“真正意义上”的异步,因为具体的读写是由操作系统代劳的。

    举个例子,将有助于理解Reactor与Proactor二者的差异,以读操作为例(写操作类似)。

    在Reactor中实现读

    • 注册读就绪事件和相应的事件处理器。
    • 事件分发器等待事件。
    • 事件到来,激活分发器,分发器调用事件对应的处理器。
    • 事件处理器完成实际的读操作,处理读到的数据,注册新的事件,然后返还控制权。

    在Proactor中实现读:

    • 处理器发起异步读操作(注意:操作系统必须支持异步IO)。在这种情况下,处理器无视IO就绪事件,它关注的是完成事件。
    • 事件分发器等待操作完成事件。
    • 在分发器等待过程中,操作系统利用并行的内核线程执行实际的读操作,并将结果数据存入用户自定义缓冲区,最后通知事件分发器读操作完成。
    • 事件分发器呼唤处理器。
    • 事件处理器处理用户自定义缓冲区中的数据,然后启动一个新的异步操作,并将控制权返回事件分发器。

    可以看出,两个模式的相同点,都是对某个I/O事件的事件通知(即告诉某个模块,这个I/O操作可以进行或已经完成)。在结构上,两者也有相同点:事件分发器负责提交IO操作(异步)、查询设备是否可操作(同步),然后当条件满足时,就回调handler;不同点在于,异步情况下(Proactor),当回调handler时,表示I/O操作已经完成;同步情况下(Reactor),回调handler时,表示I/O设备可以进行某个操作(can read 或 can write)。

    下面,我们将尝试应对为Proactor和Reactor模式建立可移植框架的挑战。在改进方案中,我们将Reactor原来位于事件处理器内的Read/Write操作移至分发器(不妨将这个思路称为“模拟异步”),以此寻求将Reactor多路同步I/O转化为模拟异步I/O。以读操作为例子,改进过程如下:

    • 注册读就绪事件和相应的事件处理器。并为分发器提供数据缓冲区地址,需要读取数据量等信息。
    • 分发器等待事件(如在select()上等待)。
    • 事件到来,激活分发器。分发器执行一个非阻塞读操作(它有完成这个操作所需的全部信息),最后调用对应处理器。
    • 事件处理器处理用户自定义缓冲区的数据,注册新的事件(当然同样要给出数据缓冲区地址,需要读取的数据量等信息),最后将控制权返还分发器。
      如我们所见,通过对多路I/O模式功能结构的改造,可将Reactor转化为Proactor模式。改造前后,模型实际完成的工作量没有增加,只不过参与者间对工作职责稍加调换。没有工作量的改变,自然不会造成性能的削弱。对如下各步骤的比较,可以证明工作量的恒定:

    标准/典型的Reactor:

    • 步骤1:等待事件到来(Reactor负责)。
    • 步骤2:将读就绪事件分发给用户定义的处理器(Reactor负责)。
    • 步骤3:读数据(用户处理器负责)。
    • 步骤4:处理数据(用户处理器负责)。

    改进实现的模拟Proactor:

    • 步骤1:等待事件到来(Proactor负责)。
    • 步骤2:得到读就绪事件,执行读数据(现在由Proactor负责)。
    • 步骤3:将读完成事件分发给用户处理器(Proactor负责)。
    • 步骤4:处理数据(用户处理器负责)。

      对于不提供异步I/O API的操作系统来说,这种办法可以隐藏Socket API的交互细节,从而对外暴露一个完整的异步接口。借此,我们就可以进一步构建完全可移植的,平台无关的,有通用对外接口的解决方案。

    代码示例如下:

    interface ChannelHandler{
          void channelReadComplate(Channel channel,byte[] data);
          void channelWritable(Channel channel);
       }
       class Channel{
         Socket socket;
         Event event;//读,写或者连接
       }
    
       //IO线程主循环:
       class IoThread extends Thread{
       public void run(){
       Channel channel;
       while(channel=Selector.select()){//选择就绪的事件和对应的连接
          if(channel.event==accept){
             registerNewChannelHandler(channel);//如果是新连接,则注册一个新的读写处理器
             Selector.interested(read);
          }
          if(channel.event==write){
             getChannelHandler(channel).channelWritable(channel);//如果可以写,则执行写事件
          }
          if(channel.event==read){
              byte[] data = channel.read();
              if(channel.read()==0)//没有读到数据,表示本次数据读完了
              {
              getChannelHandler(channel).channelReadComplate(channel,data;//处理读完成事件
              }
              if(过载保护){
              Selector.interested(read);
              }
    
          }
         }
        }
       Map<Channel,ChannelHandler> handlerMap;//所有channel的对应事件处理器
       }
    

    Selector.wakeup()

    主要作用

    解除阻塞在Selector.select()/select(long)上的线程,立即返回。

    两次成功的select之间多次调用wakeup等价于一次调用。

    如果当前没有阻塞在select上,则本次wakeup调用将作用于下一次select——“记忆”作用。

    为什么要唤醒?

    注册了新的channel或者事件。

    channel关闭,取消注册。

    优先级更高的事件触发(如定时器事件),希望及时处理。

    原理

    Linux上利用pipe调用创建一个管道,Windows上则是一个loopback的tcp连接。这是因为win32的管道无法加入select的fd set,将管道或者TCP连接加入select fd set。

    wakeup往管道或者连接写入一个字节,阻塞的select因为有I/O事件就绪,立即返回。可见,wakeup的调用开销不可忽视。

    Buffer的选择

    通常情况下,操作系统的一次写操作分为两步:

    1. 将数据从用户空间拷贝到系统空间。
    2. 从系统空间往网卡写。同理,读操作也分为两步:
      ① 将数据从网卡拷贝到系统空间;
      ② 将数据从系统空间拷贝到用户空间。

    对于NIO来说,缓存的使用可以使用DirectByteBuffer和HeapByteBuffer。如果使用了DirectByteBuffer,一般来说可以减少一次系统空间到用户空间的拷贝。但Buffer创建和销毁的成本更高,更不宜维护,通常会用内存池来提高性能。

    如果数据量比较小的中小应用情况下,可以考虑使用heapBuffer;反之可以用directBuffer。

    NIO存在的问题

    使用NIO != 高性能,当连接数<1000,并发程度不高或者局域网环境下NIO并没有显著的性能优势。

    NIO并没有完全屏蔽平台差异,它仍然是基于各个操作系统的I/O系统实现的,差异仍然存在。使用NIO做网络编程构建事件驱动模型并不容易,陷阱重重。

    推荐大家使用成熟的NIO框架,如Netty,MINA等。解决了很多NIO的陷阱,并屏蔽了操作系统的差异,有较好的性能和编程模型。

    总结

    最后总结一下到底NIO给我们带来了些什么:

    • 事件驱动模型
    • 避免多线程
    • 单线程处理多任务
    • 非阻塞I/O,I/O读写不再阻塞,而是返回0
    • 基于block的传输,通常比基于流的传输更高效
    • 更高级的IO函数,zero-copy
    • IO多路复用大大提高了Java网络应用的可伸缩性和实用性

    本文抛砖引玉,诠释了一些NIO的思想和设计理念以及应用场景,这只是从冰山一角。关于NIO可以谈的技术点其实还有很多,期待未来有机会和大家继续探讨

    展开全文
  • tomcat bio nio apr

    2018-08-17 10:28:22
    原理方面的资料都是从网上找的,并且把多个地方的整理到了一起,觉得很有意义。(后面对tomcat默认页面测试的数据是自己测出来的),tomcat 的三种模式如果用对了场合,...那在做tomcat bio nio apr 模式之前,先来...

    原理方面的资料都是从网上找的,并且把多个地方的整理到了一起,觉得很有意义。(后面对tomcat默认页面测试的数据是自己测出来的),tomcat 的三种模式如果用对了场合,性能绝对有大幅度的提升。当然调优也并不只在这一个方面,还有内存(堆内存、非堆内存、新生代内存)以及线程(最大线程、请求队列、备用线程、压缩、以及禁用dns轮询)等方面。

    那在做tomcat bio nio apr 模式之前,先来了解下 java 的一些特性吧。

    Java BIO、NIO、AIO

    同步 : 自己亲自出马持银行卡到银行取钱(使用同步IO时,Java自己处理IO读写)。

    异步 : 委托一小弟拿银行卡到银行取钱,然后给你(使用异步IO时,Java将IO读写委托给OS处理,需要将数据缓冲区地址和大小传给OS(银行卡和密码),OS需要支持异步IO操作API)。

    阻塞 : ATM排队取款,你只能等待(使用阻塞IO时,Java调用会一直阻塞到读写完成才返回)。

    非阻塞 : 柜台取款,取个号,然后坐在椅子上做其它事,等号广播会通知你办理,没到号你就不能去,你可以不断问大堂经理排到了没有,大堂经理如果说还没到你就不能去(使用非阻塞IO时,如果不能读写Java调用会马上返回,当IO事件分发器会通知可读写时再继续进行读写,不断循环直到读写完成)。

    Java对BIO、NIO、AIO的支持:

    Java BIO : 同步并阻塞,服务器实现模式为一个连接一个线程,即客户端有连接请求时服务器端就需要启动一个线程进行处理,如果这个连接不做任何事情会造成不必要的线程开销,当然可以通过线程池机制改善。

    Java NIO : 同步非阻塞,服务器实现模式为一个请求一个线程,即客户端发送的连接请求都会注册到多路复用器上,多路复用器轮询到连接有I/O请求时才启动一个线程进行处理。

    Java AIO(NIO.2) : 异步非阻塞,服务器实现模式为一个有效请求一个线程,客户端的I/O请求都是由OS先完成了再通知服务器应用去启动线程进行处理

    BIO、NIO、AIO适用场景分析:

    BIO方式适用于连接数目比较小且固定的架构,这种方式对服务器资源要求比较高,并发局限于应用中,JDK1.4以前的唯一选择,但程序直观简单易理解。

    NIO方式适用于连接数目多且连接比较短(轻操作)的架构,比如聊天服务器,并发局限于应用中,编程比较复杂,JDK1.4开始支持。

    AIO方式使用于连接数目多且连接比较长(重操作)的架构,比如相册服务器,充分调用OS参与并发操作,编程比较复杂,JDK7开始支持。


    好,下面来看看tomcat 的 bio、nio、apr 模式

    bio 
    bio(blocking I/O),顾名思义,即阻塞式I/O操作,表示Tomcat使用的是传统的Java I/O操作(即java.io包及其子包)。Tomcat在默认情况下,就是以bio模式运行的。遗憾的是,就一般而言,bio模式是三种运行模式中性能最低的一种。我们可以通过Tomcat Manager来查看服务器的当前状态。

    nio 
    是Java SE 1.4及后续版本提供的一种新的I/O操作方式(即java.nio包及其子包)。Java nio是一个基于缓冲区、并能提供非阻塞I/O操作的Java API,因此nio也被看成是non-blocking I/O的缩写。它拥有比传统I/O操作(bio)更好的并发运行性能。

    apr 
    (Apache Portable Runtime/Apache可移植运行库),是Apache HTTP服务器的支持库。你可以简单地理解为,Tomcat将以JNI的形式调用Apache HTTP服务器的核心动态链接库来处理文件读取或网络传输操作,从而大大地提高Tomcat对静态文件的处理性能。 Tomcat apr也是在Tomcat上运行高并发应用的首选模式。


    在这之前,我们先把tomcat管理界面配置起来,以便等下能更方便的观察我们的bio、nio、apr 模式

    添加manager/status用户

    vim /usr/local/apache-tomcat-7.0.47/conf/tomcat-users.xml
    <role rolename="manager-gui"/>       
    <user username="tomcat" password="15715746746" roles="manager-gui"/>
    • 1
    • 2
    • 3

    配置完重启,通过ip:port/manager/status 就可以看tomcat状态了,里面有服务器的信息及tomcat信息。

    bio server.xml 配置 (重启生效)

        <Connector port="8080" protocol="HTTP/1.1"
                   connectionTimeout="20000"
                   redirectPort="8443" />
    • 1
    • 2
    • 3

    nio server.xml 配置 (重启生效)

        <Connector port="8080" protocol="org.apache.coyote.http11.Http11NioProtocol"
                   connectionTimeout="20000"
                   redirectPort="8443" />
    • 1
    • 2
    • 3

    apr server.xml 配置 (重启生效)

        <Connector port="8080" protocol="org.apache.coyote.http11.Http11AprProtocol"
                   connectionTimeout="20000"
                   redirectPort="8443" />
    
    • 1
    • 2
    • 3
    • 4

    当然,apr模式还需要安装 apr 、 apr-utils 、tomcat-native包

    apr 安装
    tar zxf apr-1.5.2.tar.gz -C /usr/local/src/
    cd /usr/local/src/apr-1.5.2/
    ./configure --prefix=/usr/local/apr && make && make install
    
    
    apr-utils 安装
    tar zxf apr-util-1.5.4.tar.gz -C /usr/local/src/
    cd /usr/local/src/apr-util-1.5.4/
    ./configure --with-apr=/usr/local/apr/ --prefix=/usr/local/apr-utils && make && make install 
    
    
    tomcat-native安装
    cd /usr/local/apache-tomcat-7.0.65/bin/
    tar zxf tomcat-native.tar.gz 
    cd tomcat-native-1.1.33-src/jni/native
    ./configure --with-apr=/usr/local/apr --with-java-home=/usr/local/java/ && make && make install
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17

    安装完后记得在 /etc/profile 的JAVA变量后面多加一条APR的环境变量

    设置环境变量

    JAVA_HOME=/usr/local/java
    JAVA_BIN=$JAVA_HOME/bin
    PATH=$PATH:$JAVA_BIN
    CLASSPATH=$JAVA_HOME/lib/dt.jar:$JAVA_HOME/lib/tools.jar
    export JAVA_HOME JAVA_BIN PATH CLASSPATH
    export LD_LIBRARY_PATH=$LD_LIBRARY_PATH:/usr/local/apr/lib 
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6

    source /etc/profile

    那下面来看看 apr 的页面

    这里写图片描述


    到这里大致的配置就结束了,建议在做实验之前,先了解清楚java的 bio 、nio 、aio特性,在web服务器上阻塞IO(BIO)与NIO一个比较重要的不同是,客户系统使用BIO的时候往往会为每一个web请求引入多线程,每个web请求一个单独的线程,所以并发量一旦上去了,线程数就上去了,CPU就忙着线程切换,所以BIO不合适高吞吐量、高可伸缩的web服务器;而NIO则是使用单线程(单个CPU)或者只使用少量的多线程(多CPU)来接受Socket,而由线程池来处理堵塞在pipe或者队列里的请求.这样的话,只要OS可以接受TCP的连接,web服务器就可以处理该请求。大大提高了web服务器的可伸缩性。

    下面是我自己的一些性能测试表格,通过jmeter压测软件(jmeter软件使用自行百度)在本地测试的(为了数据的有效以及准确性,我没测试10分钟,也就是进行一次测试,都会重启linux主机及重新打开jmeter软件。),当然你也可以选择通过 云主机、自己机房的服务器以及虚拟机等测试,测试当中会涉及到很多点,例如(磁盘io、带宽、内存、cpu、以及内核配置中的tcp各种状态,甚至是各种打开文件限制,都会影响到我们的测试结果)

    这里写图片描述

    可以看到,随着线程的不断增多,bio 模式性能越来越差,就算是在本地,错误率和响应时间都在明显的增加、而吞吐量、样本数和每秒传输速率都在下降(当然,如果是生产环境,我们肯定通过nginx web 软件进行反向代理,提供多个tomcat 节点来提供更稳定的服务。)

    而 bio 和 apr模式基本上没有变化太多,都保持在一个稳定的状态。


    而后来当我进行一些 service 后端程序的测试时,发现 tomcat 性能并没有大幅度的提升,甚至会有下降的趋势。(该接口不是在同网段测试,而是跨越路由器,在网络传输中会有损耗方面,性能跟本地测试也会有所差异)

    这里写图片描述


    总结: 
    个人觉得在 tomcat bio、nio、apr 模式中,每种都会有各自适用的场合,也不能说哪个好那个不好,就像 tomcat 内存方面的配置,如果内存设置的过大,gc 垃圾回收机制就会变慢;如果内存设置的过小,tomcat又会出现内存溢出的情况,所以设置在一个合适的范围很重要,不仅不会出错,并且gc回收频繁使性能达到一个最优的结果。当然,这也需要根据不同的场合进行不同的测试才能产生最优的结果!

    展开全文
  • 聊聊BIO NIO AIO的区别

    2021-01-05 10:31:00
    聊聊BIO NIO AIO 的概念和使用场景

    前言:简单说说I/O

    在计算机系统中I/O就是输入(Input)和输出(Output)的意思,针对不同的操作对象,可以划分为磁盘I/O模型,网络I/O模型,内存映射I/O, Direct I/O、数据库I/O等,只要具有输入输出类型的交互系统都可以认为是I/O系统,也可以说I/O是整个操作系统数据交换与人机交互的通道,这个概念与选用的开发语言没有关系,是一个通用的概念。

    在如今的系统中I/O却拥有很重要的位置,现在系统都有可能处理大量文件,大量数据库操作,而这些操作都依赖于系统的I/O性能,也就造成了现在系统的瓶颈往往都是由于I/O性能造成的。因此,为了解决磁盘I/O性能慢的问题,系统架构中添加了缓存来提高响应速度;或者有些高端服务器从硬件级入手,使用了固态硬盘(SSD)来替换传统机械硬盘;在大数据方面,Spark越来越多的承担了实时性计算任务,而传统的Hadoop体系则大多应用在了离线计算与大量数据存储的场景,这也是由于磁盘I/O性能远不如内存I/O性能而造成的格局(Spark更多的使用了内存,而MapReduece更多的使用了磁盘)。因此,一个系统的优化空间,往往都在低效率的I/O环节上,很少看到一个系统CPU、内存的性能是其整个系统的瓶颈。也正因为如此,Java在I/O上也一直在做持续的优化,从JDK 1.4开始便引入了NIO模型,大大的提高了以往BIO模型下的操作效率。

    进程中的IO调用步骤大致可以分为以下四步:

    1. 进程向操作系统请求数据 ;
    2. 操作系统把外部数据加载到内核的缓冲区中;
    3. 操作系统把内核的缓冲区拷贝到进程的缓冲区 ;
    4. 进程获得数据完成自己的功能 ;

    关键词:同步阻塞 同步非阻塞 异步阻塞

    BIO

    什么是BIO?

    BIO:(blocking I/O) 同步阻塞I/O模式,JDK1.4以前的唯一选择。数据的读取写入必须阻塞在一个线程内等待其完成。

    服务器实现模式为一个连接一个线程,即客户端有连接请求时服务器端就需要启动一个线程进行处理,如果这个连接不做任何事情会造成不必要的线程开销,当然可以通过线程池机制改善。

    例:这里假设一个烧开水的场景,有一排水壶在烧开水,BIO的工作模式就是, 叫一个线程停留在一个水壶那,直到这个水壶烧开,才去处理下一个水壶。但是实际上线程在等待水壶烧开的时间段什么都没有做。

    BIO的特点

    同步阻塞I/O。这是最基本与简单的I/O操作方式,其根本特性是做完一件事再去做另一件事,一件事一定要等前一件事做完,这很符合程序员传统的开发思想,因此BIO模型的程序开发起来较为简单,易于把握。

    但是BIO如果需要同时做很多事情(例如同时读很多文件,处理很多tcp请求等),就需要系统创建很多线程来完成对应的工作,因为BIO模型下一个线程同时只能做一个工作,如果线程在执行过程中依赖于需要等待的资源,那么该线程会长期处于阻塞状态,我们知道在整个操作系统中,线程是系统执行的基本单位,在BIO模型下的线程 阻塞就会导致系统线程的切换,从而对整个系统性能造成一定的影响。当然如果我们只需要创建少量可控的线程,那么采用BIO模型也是很好的选择,但如果在需要考虑高并发的web或者tcp服务器中采用BIO模型就无法应对了,如果系统开辟成千上万的线程,那么CPU的执行时机都会浪费在线程的切换中,使得线程的执行效率大大降低。此外,关于线程这里说一句题外话,在系统开发中线程的生命周期一定要准确控制,在需要一定规模并发的情形下,尽量使用线程池来确保线程创建数目在一个合理的范围之内,切莫编写线程数量创建上限的代码。

    NIO

    什么是NIO?

    NIO:(non-blocking I/O) 支持阻塞与非阻塞模式,JDK1.4开始支持。但这里我们以其同步非阻塞I/O模式来说明,那么什么叫做同步非阻塞?

    服务器实现模式为一个请求一个线程,即客户端发送的连接请求都会注册到多路复用器Selector上,多路复用器轮询到连接有I/O请求时才启动一个线程进行处理。

    例:还拿烧开水来说,NIO的做法是叫一个线程不断的轮询每个水壶的状态,看看是否有水壶的状态发生了改变,从而进行下一步的操作。

    NIO的特点

    同步非阻塞I/O。关于NIO,国内有很多技术博客将英文翻译成No-Blocking I/O,非阻塞I/O模型 ,当然这样就与BIO形成了鲜明的特性对比。NIO本身是基于事件驱动的思想来实现的,其目的就是解决BIO的大并发问题,在BIO模型中,如果需要并发处理多个I/O请求,那就需要多线程来支持,NIO使用了多路复用器机制,以socket使用来说,多路复用器通过不断轮询各个连接的状态,只有在socket有流可读或者可写时,应用程序才需要去处理它,在线程的使用上,就不需要一个连接就必须使用一个处理线程了,而是只是有效请求时(确实需要进行I/O处理时),才会使用一个线程去处理,这样就避免了BIO模型下大量线程处于阻塞等待状态的情景。

    NIO抽象出了新的通道(Channel)作为输入输出的通道,并且提供了缓存(Buffer)的支持,在进行读操作时,需要使用Buffer分配空间,然后将数据从Channel中读入Buffer中,对于Channel的写操作,也需要现将数据写入Buffer,然后将Buffer写入Channel中。

    新的IO操作不再面向 Stream来进行操作了,改为了通道Channel,并且使用了更加灵活的缓存区类Buffer,Buffer只是缓存区定义接口, 根据需要,我们可以选择对应类型的缓存区实现类。在java NIO编程中,我们需要理解以下3个对象Channel、Buffer和Selector。

    • Channel
      首先说一下Channel,国内大多翻译成“通道”。Channel和IO中的Stream(流)是差不多一个等级的。只不过Stream是单向的,譬如:InputStream, OutputStream。而Channel是双向的,既可以用来进行读操作,又可以用来进行写操作,NIO中的Channel的主要实现有:FileChannel、DatagramChannel、SocketChannel、ServerSocketChannel;通过看名字就可以猜出个所以然来:分别可以对应文件IO、UDP和TCP(Server和Client)。

    • Buffer
      NIO中的关键Buffer实现有:ByteBuffer、CharBuffer、DoubleBuffer、 FloatBuffer、IntBuffer、 LongBuffer,、ShortBuffer,分别对应基本数据类型: byte、char、double、 float、int、 long、 short。当然NIO中还有MappedByteBuffer, HeapByteBuffer, DirectByteBuffer等这里先不具体陈述其用法细节。

    • Selector
      Selector 是NIO相对于BIO实现多路复用的基础,Selector 运行单线程处理多个 Channel,如果你的应用打开了多个通道,但每个连接的流量都很低,使用 Selector 就会很方便。例如在一个聊天服务器中。要使用 Selector , 得向 Selector 注册 Channel,然后调用它的 select() 方法。这个方法会一直阻塞到某个注册的通道有事件就绪。一旦这个方法返回,线程就可以处理这些事件,事件的例子有如新的连接进来、数据接收等。

    AIO

    什么是AIO?

    AIO:(Asynchronous I/O) 异步非阻塞I/O模型,JDK1.7开始支持。异步非阻塞与同步非阻塞的区别在哪里?异步非阻塞无需一个线程去轮询所有IO操作的状态改变,在相应的状态改变后,系统会通知对应的线程来处理。

    服务器实现模式为一个有效请求一个线程,客户端的I/O请求都是由OS先完成了再通知服务器应用去启动线程进行处理。

    例:对应到烧开水中就是,为每个水壶上面装了一个开关,水烧开之后,水壶会自动通知我水烧开了。

    AIO的特点

    异步非阻塞I/O。Java AIO就是Java作为对异步IO提供支持的NIO.2 ,Java NIO2 (JSR 203)定义了更多的 New I/O APIs, 提案2003提出,直到2011年才发布, 最终在JDK 7中才实现。JSR 203除了提供更多的文件系统操作API(包括可插拔的自定义的文件系统), 还提供了对socket和文件的异步 I/O操作。 同时实现了JSR-51提案中的socket channel全部功能,包括对绑定, option配置的支持以及多播multicast的实现

    BIO NIO AIO 的使用场景

    BIO方式:适用于连接数目比较小且固定的架构,这种方式对服务器资源要求比较高,并发局限于应用中,JDK1.4以前的唯一选择,但程序直观简单易理解。

    NIO方式适用于连接数目多且连接比较短(轻操作)的架构,比如聊天服务器,并发局限于应用中,编程比较复杂,JDK1.4开始支持。

    AIO方式使用于连接数目多且连接比较长(重操作)的架构,比如相册服务器,充分调用OS参与并发操作,编程比较复杂,JDK7开始支持。

    并发连接数不多时采用BIO,因为它编程和调试都非常简单,但如果涉及到高并发的情况,应选择NIO或AIO,更好的建议是采用成熟的网络通信框架Netty。

    小结

    IO实质上与线程没有太多的关系,但是不同的IO模型改变了应用程序使用线程的方式,NIO与AIO的出现解决了很多BIO无法解决的并发问题,当然任何技术抛开适用场景都是耍流氓,复杂的技术往往是为了解决简单技术无法解决的问题而设计的,在系统开发中能用常规技术解决的问题,绝不用复杂技术,否则大大增加系统代码的维护难度,学习IT技术不是为了炫技,而是要实实在在解决问题。

    展开全文
  • JAVA IO : BIO NIO AIO

    千次阅读 2018-11-05 14:19:55
    JAVA IO : BIO NIO AIO同步异步、阻塞非阻塞概念同步与异步阻塞与非阻塞IO VS NIO VS AIO面向流与面向缓冲阻塞与非阻塞IOBIO、NIO、AIO的JAVA实现BIONIO、AIO适用场景分析 同步异步、阻塞非阻塞概念 同步和异步是...
  • BIO NIO AIO

    2018-08-09 18:11:07
    根据自己的理解,总结对比了一下BIONIO、AIO。 BIO:线程发起IO请求,不管内核是否准备好IO操作,从发起请求起,线程一直阻塞,直到操作完成。如下图: NIO(reactor模型):线程发起IO请求,立即返回;内核在...
  • IO的方式通常分为几种,同步阻塞的BIO、同步非阻塞的NIO、异步非阻塞的AIO。 一、BIO 在JDK1.4出来之前,我们建立网络连接的时候采用BIO模式,需要先在服务端启动一个ServerSocket,然后在客户端启动Socket来对...
  • Java BIO NIO AIO

    2017-11-21 13:59:56
    标签:Java BIO NIO AIO 原创作品,允许转载,转载时请务必以超链接形式标明文章 原始出处 、作者信息和本声明。否则将追究法律责任。http://stevex.blog.51cto.com/4300375/1284437 前言:关于BIO/NIO/...
  • BIO NIO AIO 区别

    2021-05-03 04:10:01
    NIO 同步非阻塞 线程主动调用轮询询问是否有accept,read,write发生(同步),因为在调用accept,read,write时已经发生事件,所以不会去阻塞等待(基于多路复用器实现)(非阻塞) AIO 异步非阻塞 主线程不主动调用或...
  • java bio nio aio

    2016-01-25 12:06:12
    前言:关于BIO/NIO/AIO的文章已经汗牛充栋,俺最近比较闲试图系统学习一下,希望大侠多多指教! 先来个例子理解一下概念,以银行取款为例: 同步 : 自己亲自出马持银行卡到银行取钱(使用同步IO时...
  • 简论BIO NIO AIO

    2017-06-10 15:24:44
    IO的方式通常分为几种,同步阻塞的BIO、同步非阻塞的NIO、异步非阻塞的AIO。
  • bio nio aio的理解

    千次阅读 2018-06-19 11:45:09
    看来很多关于bio nio aio博客,每次都是看不太懂,终于有点头绪,写博客记录下备用。在了解bio nio aio之前,首先这三个都是针对系统io来说的(包括网络io、磁盘io、内存io),先要知道程序读取数据是分两步走,第一步...
  • BIO NIO AIO 多路复用

    2021-03-24 10:46:24
    相比于BIO 用户线程请求内核后立马收到返回,用户线程为非阻塞状态,可以执行别的操作,但是需要不停的轮询调用内核,查看数据是否准备完成,比较耗费CPU性能 多路复用 NIO 形成原因: 如果一个I/O流进来,我们就开启一个...
  • Java bio nio aio

    2016-03-30 20:04:12
    在弄清楚上面的几个问题之前,我们首先得明白什么是同步,异步,阻塞,非阻塞,只有这几个单个概念理解清楚了,然后在组合理解起来,就相对比较容易了。 同步和异步是针对应用程序和内核的交互而言的。 阻塞...
  • 面试题-17AIO BIO NIO

    2021-03-01 17:35:44
    BIO,NIO,AIO 总结 Java 中的 BIONIO和 AIO 理解为是 Java 语言对操作系统的各种 IO 模型的封装。程序员在使用这些 API 的时候,不需要关心操作系统层面的知识,也不需要根据不同操作系统编写不同的代码。只需要...
  • java BIO NIO AIO 理论篇

    2016-04-16 21:54:45
    java BIO NIO AIO 理论篇 http://qindongliang1922.iteye.com/blog/2018539 在高性能的IO体系设计中,有几个名词概念常常会使我们感到迷惑不解。具体如下:  序号 问题 1 什么是同步? ...
  • java BIO NIO AIO

    2018-04-02 15:51:37
    首先来说一下什么是I/O? 在计算机系统中I/O就是输入(Input)和输出(Output)的意思,针对不同的操作对象,可以划分为磁盘I/O模型,网络I/O模型,内存映射I/O, Direct I/O、数据库I/O等,只要具有输入输出类型的交互...
  • 那在做tomcat bio nio apr 模式之前,先来了解下 java 的一些特性吧。 Java BIONIO、AIO 同步 : 自己亲自出马持银行卡到银行取钱(使用同步IO时,Java自己处理IO读写)。 异步 : 委托一小弟拿银行卡到银行取...
  • Java网络IO BIO NIO AIO

    2016-04-11 18:23:21
    程老师博客原文:http://flychao88.iteye.com/blog/1958431NIO和IO的区别Java NIO编程 提供了对块IO的支持,使用块IO的好处是效率更高 1. 基本 概念 IO 是主存和外部设备 ( 硬盘、终端和网络等 ) 拷贝数据的...
  • 原文链接:tomcat bio nio apr 模式性能测试以及自己的看法 ...
  • JAVA bio nio aio

    2016-06-08 14:01:00
    【转自】...1 什么是同步? 2 什么是异步? 3 什么是阻塞? 4 什么是非阻塞? 5 什么是同步阻塞? 6 什么是同步非阻塞? 7 什么是异步阻塞? 8 什么是异步非阻塞? 散仙不才,在查了一部...
  • JAVA AIO BIO NIO

    2016-03-22 10:21:45
    在高性能的IO体系设计中,有...1 什么是同步? 2 什么是异步? 3 什么是阻塞? 4 什么是非阻塞? 5 什么是同步阻塞? 6 什么是同步非阻塞? 7 什么是异步阻塞? 8 什么是异步非阻塞? 散仙不才,在查了一
  • Java BIO NIO AIO 详解

    2020-03-10 08:09:57
    NIO的读写函数可以立刻返回,这就给了我们不开线程利用CPU的最好机会:如果一个连接不能读写(socket.read()返回0或者socket.write()返回0),我们可以把这件记下来,记录的方式通常是在Selector上注册标记位,...

空空如也

空空如也

1 2 3 4 5 ... 20
收藏数 5,344
精华内容 2,137
关键字:

bionio什么事