加入收藏 | 设为首页 | 会员中心 | 我要投稿 济南站长网 (https://www.0531zz.com/)- 科技、建站、经验、云计算、5G、大数据,站长网!
当前位置: 首页 > 教程 > 正文

解密:有人要将“高并发”拉下“神坛”

发布时间:2018-07-06 22:19:02 所属栏目:教程 来源:王宝令
导读:副标题#e# 【资讯】高并发也算是这几年的热门词汇了,尤其在互联网圈,开口不聊个高并发问题,都不好意思出门。 高并发有那么邪乎吗?动不动就千万并发、亿级流量,听上去的确挺吓人。但仔细想想,这么大的并发与流量不都是通过路由器来的吗? 一切源自网卡
副标题[/!--empirenews.page--]

  【资讯】高并发也算是这几年的热门词汇了,尤其在互联网圈,开口不聊个高并发问题,都不好意思出门。

  解密:有人要将“高并发”拉下“神坛”

  高并发有那么邪乎吗?动不动就千万并发、亿级流量,听上去的确挺吓人。但仔细想想,这么大的并发与流量不都是通过路由器来的吗?

  一切源自网卡

  高并发的流量通过低调的路由器进入我们系统,第一道关卡就是网卡,网卡怎么抗住高并发?

  这个问题压根就不存在,千万并发在网卡看来,一样一样的,都是电信号,网卡眼里根本区分不出来你是千万并发还是一股洪流,所以衡量网卡牛不牛都说带宽,从来没有并发量的说法。

  网卡位于物理层和链路层,最终把数据传递给网络层(IP 层),在网络层有了 IP 地址,已经可以识别出你是千万并发了。

  所以搞网络层的可以自豪的说,我解决了高并发问题,可以出来吹吹牛了。谁没事搞网络层呢?主角就是路由器,这玩意主要就是玩儿网络层。

  一头雾水

  非专业的我们,一般都把网络层(IP 层)和传输层(TCP 层)放到一起,操作系统提供,对我们是透明的,很低调、很靠谱,以至于我们都把它忽略了。

  吹过的牛是从应用层开始的,应用层一切都源于 Socket,那些千万并发最终会经过传输层变成千万个 Socket,那些吹过的牛,不过就是如何快速处理这些 Socket。处理 IP 层数据和处理 Socket 究竟有啥不同呢?

  没有连接,就没有等待

  最重要的一个不同就是 IP 层不是面向连接的,而 Socket 是面向连接的。IP 层没有连接的概念,在 IP 层,来一个数据包就处理一个,不用瞻前也不用顾后。

  而处理 Socket,必须瞻前顾后,Socket 是面向连接的,有上下文的,读到一句我爱你,激动半天,你不前前后后地看看,就是瞎激动了。

  你想前前后后地看明白,就要占用更多的内存去记忆,就要占用更长的时间去等待;不同连接要搞好隔离,就要分配不同的线程(或者协程)。所有这些都解决好,貌似还是有点难度的。

  感谢操作系统

  操作系统是个好东西,在 Linux 系统上,所有的 IO 都被抽象成了文件,网络 IO 也不例外,被抽象成 Socket。

  但是 Socket 还不仅是一个 IO 的抽象,它同时还抽象了如何处理 Socket,最著名的就是 select 和 epoll 了。

  知名的 Nginx、Netty、Redis 都是基于 epoll 做的,这仨家伙基本上是在千万并发领域的必备神技。

  但是多年前,Linux 只提供了 select,这种模式能处理的并发量非常小,而 epoll 是专为高并发而生的,感谢操作系统。

  不过操作系统没有解决高并发的所有问题,只是让数据快速地从网卡流入我们的应用程序,如何处理才是老大难。

  操作系统的使命之一就是最大限度的发挥硬件的能力,解决高并发问题,这也是最直接、最有效的方案,其次才是分布式计算。

  前面我们提到的 Nginx、Netty、Redis 都是最大限度发挥硬件能力的典范。如何才能最大限度的发挥硬件能力呢?

  核心矛盾

  要最大限度的发挥硬件能力,首先要找到核心矛盾所在。我认为,这个核心矛盾从计算机诞生之初直到现在,几乎没有发生变化,就是 CPU 和 IO 之间的矛盾。

  CPU 以摩尔定律的速度野蛮发展,而 IO 设备(磁盘,网卡)却乏善可陈。龟速的 IO 设备成为性能瓶颈,必然导致 CPU 的利用率很低,所以提升 CPU 利用率几乎成了发挥硬件能力的代名词。

  中断与缓存

  CPU 与 IO 设备的协作基本都是以中断的方式进行的,例如读磁盘的操作,CPU 仅仅是发一条读磁盘到内存的指令给磁盘驱动,之后就立即返回了。

  此时 CPU 可以接着干其他事情,读磁盘到内存本身是个很耗时的工作,等磁盘驱动执行完指令,会发个中断请求给 CPU,告诉 CPU 任务已经完成,CPU 处理中断请求,此时 CPU 可以直接操作读到内存的数据。

  中断机制让 CPU 以最小的代价处理 IO 问题,那如何提高设备的利用率呢?答案就是缓存。

  操作系统内部维护了 IO 设备数据的缓存,包括读缓存和写缓存。读缓存很容易理解,我们经常在应用层使用缓存,目的就是尽量避免产生读 IO。

  写缓存应用层使用的不多,操作系统的写缓存,完全是为了提高 IO 写的效率。

  操作系统在写 IO 的时候会对缓存进行合并和调度,例如写磁盘会用到电梯调度算法。

  高效利用网卡

  高并发问题首先要解决的是如何高效利用网卡。网卡和磁盘一样,内部也是有缓存的,网卡接收网络数据,先存放到网卡缓存,然后写入操作系统的内核空间(内存),我们的应用程序则读取内存中的数据,然后处理。

  除了网卡有缓存外,TCP/IP 协议内部还有发送缓冲区和接收缓冲区以及 SYN 积压队列、accept 积压队列。

  这些缓存,如果配置不合适,则会出现各种问题。例如在 TCP 建立连接阶段,如果并发量过大,而 Nginx 里面 Socket 的 backlog 设置的值太小,就会导致大量连接请求失败。

  如果网卡的缓存太小,当缓存满了后,网卡会直接把新接收的数据丢掉,造成丢包。

  当然如果我们的应用读取网络 IO 数据的效率不高,会加速网卡缓存数据的堆积。如何高效读取网络数据呢?目前在 Linux 上广泛应用的就是 epoll 了。

  操作系统把 IO 设备抽象为文件,网络被抽象成了 Socket,Socket 本身也是一个文件,所以可以用 read/write 方法来读取和发送网络数据。在高并发场景下,如何高效利用 Socket 快速读取和发送网络数据呢?

  要想高效利用 IO,就必须在操作系统层面了解 IO 模型,在《UNIX网络编程》这本经典著作里总结了五种 IO 模型,分别是:

  阻塞式 IO

  非阻塞式 IO

  多路复用 IO

  信号驱动 IO

  异步 IO

  阻塞式 IO

  我们以读操作为例,当我们调用 read 方法读取 Socket 上的数据时,如果此时 Socket 读缓存是空的(没有数据从 Socket 的另一端发过来),操作系统会把调用 read 方法的线程挂起,直到 Socket 读缓存里有数据时,操作系统再把该线程唤醒。

  当然,在唤醒的同时,read 方法也返回了数据。我理解所谓的阻塞,就是操作系统是否会挂起线程。

  非阻塞式 IO

  而对于非阻塞式 IO,如果 Socket 的读缓存是空的,操作系统并不会把调用 read 方法的线程挂起,而是立即返回一个 EAGAIN 的错误码。

  在这种情景下,可以轮询 read 方法,直到 Socket 的读缓存有数据则可以读到数据,这种方式的缺点非常明显,就是消耗大量的 CPU。

  多路复用 IO

  对于阻塞式 IO,由于操作系统会挂起调用线程,所以如果想同时处理多个 Socket,就必须相应地创建多个线程。

  线程会消耗内存,增加操作系统进行线程切换的负载,所以这种模式不适合高并发场景。有没有办法较少线程数呢?

  非阻塞 IO 貌似可以解决,在一个线程里轮询多个 Socket,看上去可以解决线程数的问题,但实际上这个方案是无效的。

  原因是调用 read 方法是一个系统调用,系统调用是通过软中断实现的,会导致进行用户态和内核态的切换,所以很慢。

  但是这个思路是对的,有没有办法避免系统调用呢?有,就是多路复用 IO。

  在 Linux 系统上 select/epoll 这俩系统 API 支持多路复用 IO,通过这两个 API,一个系统调用可以监控多个 Socket,只要有一个 Socket 的读缓存有数据了,方法就立即返回。

  然后你就可以去读这个可读的 Socket 了,如果所有的 Socket 读缓存都是空的,则会阻塞,也就是将调用 select/epoll 的线程挂起。

  所以 select/epoll 本质上也是阻塞式 IO,只不过它们可以同时监控多个 Socket。

  select 和 epoll 的区别

  为什么多路复用 IO 模型有两个系统 API?我分析原因是,select 是 POSIX 标准中定义的,但是性能不够好,所以各个操作系统都推出了性能更好的 API,如 Linux 上的 epoll、Windows 上的 IOCP。

  至于 select 为什么会慢,大家比较认可的原因有两点:

  一点是 select 方法返回后,需要遍历所有监控的 Socket,而不是发生变化的 Socket。

  还有一点是每次调用 select 方法,都需要在用户态和内核态拷贝文件描述符的位图(通过调用三次 copy_from_user 方法拷贝读、写、异常三个位图)。

  epoll 可以避免上面提到的这两点。

  Reactor 多线程模型

  在 Linux 操作系统上,性能最为可靠、稳定的 IO 模式就是多路复用,我们的应用如何能够利用好多路复用 IO 呢?

  经过前人多年实践总结,搞了一个 Reactor 模式,目前应用非常广泛,著名的 Netty、Tomcat NIO 就是基于这个模式。

  Reactor 的核心是事件分发器和事件处理器,事件分发器是连接多路复用 IO 和网络数据处理的中枢,监听 Socket 事件(select/epoll_wait)。

  然后将事件分发给事件处理器,事件分发器和事件处理器都可以基于线程池来做。

  需要重点提一下的是,在 Socket 事件中主要有两大类事件,一个是连接请求,另一个是读写请求,连接请求成功处理之后会创建新的 Socket,读写请求都是基于这个新创建的 Socket。

  所以在网络处理场景中,实现 Reactor 模式会稍微有点绕,但是原理没有变化。

(编辑:济南站长网)

【声明】本站内容均来自网络,其相关言论仅代表作者个人观点,不代表本站立场。若无意侵犯到您的权利,请及时与联系站长删除相关内容!

热点阅读