操做系统做为底层硬件和上层应用的中间层,使命之一就是最大限度的发挥硬件能力,解决高并发问题。在Linux上,全部的IO都被抽象成了文件,知名的高并发神器nginx、netty、redis都是基于它的epoll搞的。react
高速CPU和低俗IO之间的矛盾是最大限度发挥硬件能力的核心矛盾;CPU和IO设备的协做基本都是以中断的方式进行,CPU发一条读磁盘到内存的指令给磁盘驱动后就当即返回作其它事,磁盘驱动执行完耗时的读磁盘到内存工做后会发个中断请求给CPU,CPU处理中断请求操做读到内存的数据。nginx
中断机制让CPU以最小的代价处理IO问题,缓存机制尽量的提升设备的利用率。操做系统维护了IO设备数据的缓存,包括读缓存和写缓存,在写IO的时候会对缓存进行合并和调度,如写磁盘时会用到电梯调度算法。redis
在Linux上性能最可靠稳定的IO模式是多路复用,经多人实践总结,搞了一个Recactor模式,Netty、Tomcat NIO就是基于这个模式。算法
>>>>数据库
0x00 一切源自网卡编程
高并发的流量经过低调的路由器进入咱们系统,第一道关卡就是网卡,网卡怎么抗住高并发?这个问题压根就不存在,千万并发在网卡看来,同样同样的,都是电信号,网卡眼里根本区分不出来你是千万并发仍是一股洪流,因此衡量网卡牛不牛都说带宽,历来没有并发量的说法。设计模式
网卡位于物理层和链路层,最终把数据传递给网络层(IP层),在网络层有了IP地址,已经能够识别出你是千万并发了,因此搞网络层的能够自豪的说,我解决了高并发问题,能够出来吹吹牛了。谁没事搞网络层呢?主角就是路由器,这玩意主要就是玩儿网络层。缓存
>>>>安全
0x01 一头雾水网络
非专业的咱们,通常都把网络层(IP层)和传输层(TCP层)放到一块儿,操做系统提供,对咱们是透明的,很低调、很靠谱,以致于咱们都把他忽略了。
吹过的牛是从应用层开始的,应用层一切都源于Socket,那些千万并发最终会通过传输层变成千万个Socket,那些吹过的牛,不过就是如何快速处理这些Socket。处理IP层数据和处理Socket究竟有啥不一样呢?
>>>>
0x02 没有链接,就没用等待
最重要的一个不一样就是IP层不是面向链接的,而Socket是面向链接的,IP层没有链接的概念,在IP层,来一个数据包就处理一个,不用瞻前也不用顾后;而处理Socket,必须瞻前顾后,Socket是面向链接的,有上下文的,读到一句我爱你,激动半天,你不前先后后地看看,就是瞎激动了。
你想前先后后地看明白,就要占用更多的内存去记忆,就要占用更长的时间去等待;不一样链接要搞好隔离,就要分配不一样的线程(或者协程)。全部这些都解决好,貌似仍是有点难度的。
>>>>
0x03 感谢操做系统
操做系统是个好东西,在Linux系统上,全部的IO都被抽象成了文件,网络IO也不例外,被抽象成Socket,可是Socket还不只是一个IO的抽象,它同时还抽象了如何处理Socket,最著名的就是select和epoll了,知名的nginx、netty、redis都是基于epoll搞的,这仨家伙基本上是在千万并发领域必备神技。
可是多年前,Linux只提供了select的,这种模式能处理的并发量很是小,而epoll是专为高并发而生的,感谢操做系统。不过操做系统没有解决高并发的全部问题,只是让数据快速地从网卡流入咱们的应用程序,如何处理才是老大难。
操做系统的使命之一就是最大限度的发挥硬件的能力,解决高并发问题,这也是最直接、最有效的方案,其次才是分布式计算。前面咱们提到的nginx、netty、redis都是最大限度发挥硬件能力的典范。如何才能最大限度的发挥硬件能力呢?
>>>>0x04 核心矛盾
要最大限度的发挥硬件能力,首先要找到核心矛盾所在。我认为,这个核心矛盾从计算机诞生之初直到如今,几乎没有发生变化,就是CPU和IO之间的矛盾。
CPU以摩尔定律的速度野蛮发展,而IO设备(磁盘,网卡)却乏善可陈。龟速的IO设备成为性能瓶颈,必然致使CPU的利用率很低,因此提高CPU利用率几乎成了发挥硬件能力的代名词。
>>>>
0x05 中断与缓存
CPU与IO设备的协做基本都是以中断的方式进行的,例如读磁盘的操做,CPU仅仅是发一条读磁盘到内存的指令给磁盘驱动,以后就当即返回了,此时CPU能够接着干其余事情,读磁盘到内存自己是个很耗时的工做,等磁盘驱动执行完指令,会发个中断请求给CPU,告诉CPU任务已经完成,CPU处理中断请求,此时CPU能够直接操做读到内存的数据。
中断机制让CPU以最小的代价处理IO问题,那如何提升设备的利用率呢?答案就是缓存。
操做系统内部维护了IO设备数据的缓存,包括读缓存和写缓存,读缓存很容易理解,咱们常常在应用层使用缓存,目的就是尽可能避免产生读IO。
写缓存应用层使用的很少,操做系统的写缓存,彻底是为了提升IO写的效率。操做系统在写IO的时候会对缓存进行合并和调度,例如写磁盘会用到电梯调度算法。
>>>>
0x06 高效利用网卡
高并发问题首先要解决的是如何高效利用网卡。网卡和磁盘同样,内部也是有缓存的,网卡接收网络数据,先存放到网卡缓存,而后写入操做系统的内核空间(内存),咱们的应用程序则读取内存中的数据,而后处理。
除了网卡有缓存外,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。
>>>>
0x07 阻塞式IO
咱们以读操做为例,当咱们调用read方法读取Socket上的数据时,若是此时Socket读缓存是空的(没有数据从Socket的另外一端发过来),操做系统会把调用read方法的线程挂起,直到Socket读缓存里有数据时,操做系统再把该线程唤醒。
固然,在唤醒的同时,read方法也返回了数据。我理解所谓的阻塞,就是操做系统是否会挂起线程。
>>>>
0x08 非阻塞式IO
而对于非阻塞式IO,若是Socket的读缓存是空的,操做系统并不会把调用read方法的线程挂起,而是当即返回一个EAGAIN的错误码,在这种情景下,能够轮询read方法,直到Socket的读缓存有数据则能够读到数据,这种方式的缺点很是明显,就是消耗大量的CPU。
>>>>
0x09 多路复用IO
对于阻塞式IO,因为操做系统会挂起调用线程,因此若是想同时处理多个Socket,就必须相应地建立多个线程,线程会消耗内存,增长操做系统进行线程切换的负载,因此这种模式不适合高并发场景。有没有办法较少线程数呢?
非阻塞IO貌似能够解决,在一个线程里轮询多个Socket,看上去能够解决线程数的问题,但实际上这个方案是无效的,缘由是调用read方法是一个系统调用,系统调用是经过软中断实现的,会致使进行用户态和内核态的切换,因此很慢。
可是这个思路是对的,有没有办法避免系统调用呢?有,就是多路复用IO。
在Linux系统上select/epoll这俩系统API支持多路复用IO,经过这两个API,一个系统调用能够监控多个Socket,只要有一个Socket的读缓存有数据了,方法就当即返回,而后你就能够去读这个可读的Socket了,若是全部的Socket读缓存都是空的,则会阻塞,也就是将调用select/epoll的线程挂起。
因此select/epoll本质上也是阻塞式IO,只不过他们能够同时监控多个Socket。
>>>>
0x0A select和epoll的区别
为何多路复用IO模型有两个系统API?我分析缘由是,select是POSIX标准中定义的,可是性能不够好,因此各个操做系统都推出了性能更好的API,如Linux上的epoll、Windows上的IOCP。
至于select为何会慢,你们比较承认的缘由有两点,一点是select方法返回后,须要遍历全部监控的Socket,而不是发生变化的Ssocket,还有一点是每次调用select方法,都须要在用户态和内核态拷贝文件描述符的位图(经过调用三次copy_from_user方法拷贝读、写、异常三个位图)。epoll能够避免上面提到的这两点。
>>>>
0x0B Reactor多线程模型
在Linux操做系统上,性能最为可靠、稳定的IO模式就是多路复用,咱们的应用如何可以利用好多路复用IO呢?通过前人多年实践总结,搞了一个Reactor模式,目前应用很是普遍,著名的Netty、Tomcat NIO就是基于这个模式。
Reactor的核心是事件分发器和事件处理器,事件分发器是链接多路复用IO和网络数据处理的中枢,核心就是监听Socket事件(select/epoll_wait),而后将事件分发给事件处理器,事件分发器和事件处理器均可以基于线程池来作。
须要重点提一下的是,在Socket事件中主要有两大类事件,一个是链接请求,另外一个是读写请求,链接请求成功处理以后会建立新的Socket,读写请求都是基于这个新建立的Socket。
因此在网络处理场景中,实现Reactor模式会稍微有点绕,可是原理没有变化。具体实现能够参考Doug Lea的《Scalable IO in Java》(http://gee.cs.oswego.edu/dl/cpjslides/nio.pdf)
Reactor原理图
>>>>
0x0C Nginx多进程模型
Nginx默认采用的是多进程模型,Nginx分为Master进程和Worker进程,真正负责监听网络请求并处理请求的只有Worker进程,全部的Worker进程都监听默认的80端口,可是每一个请求只会被一个Worker进程处理。
这里面的玄机是:每一个进程在accept请求前必须争抢一把锁,获得锁的进程才有权处理当前的网络请求。每一个Worker进程只有一个主线程,单线程的好处是无锁处理,无锁处理并发请求,这基本上是高并发场景里面的最高境界了。(参考http://www.dre.vanderbilt.edu/~schmidt/PDF/reactor-siemens.pdf)
数据通过网卡、操做系统、网络协议中间件(Tomcat、Netty等)重重关卡,终于到了咱们应用开发人员手里,咱们如何处理这些高并发的请求呢?咱们仍是先从提高单机处理能力的角度来思考这个问题。
>>>>0x0D 突破木桶理论
据通过网卡、操做系统、中间件(Tomcat、Netty等)重重关卡,终于到了咱们应用开发人员手里,咱们如何处理这些高并发的请求呢?
咱们仍是先从提高单机处理能力的角度来思考这个问题,在实际应用的场景中,问题的焦点是如何提升CPU的利用率(谁叫它发展的最快呢),木桶理论讲最短的那根板决定水位,那为啥不是提升短板IO的利用率,而是去提升CPU的利用率呢?
这个问题的答案是在实际应用中,提升了CPU的利用率每每会同时提升IO的利用率。固然在IO利用率已经接近极限的条件下,再提升CPU利用率是没有意义的。咱们先来看看如何提升CPU的利用率,后面再看如何提升IO的利用率。
>>>>0x0E 并行与并发
提高CPU利用率目前主要的方法是利用CPU的多核进行并行计算,并行和并发是有区别的,在单核CPU上,咱们能够一边听MP3,一边Coding,这个是并发,但不是并行,由于在单核CPU的视野,听MP3和Coding是不可能同时进行的。
只有在多核时代,才会有并行计算。并行计算这东西过高级,工业化应用的模型主要有两种,一种是共享内存模型,另一种是消息传递模型。
>>>>
0x0F 多线程设计模式
对于共享内存模型,其原理基本都来自大师Dijkstra在半个世纪前(1965)的一篇论文《Cooperating sequential processes》,这篇论文提出了大名鼎鼎的概念信号量,Java里面用于线程同步的wait/notify也是信号量的一种实现。
大师的东西看不懂,学不会也不用以为丢人,毕竟大师的嫡传子弟也没几个。东洋有个叫结城浩的总结了一下多线程编程的经验,写了本书叫《JAVA多线程设计模式》,这个仍是挺接地气(能看懂)的。下面简单介绍一下。
1. Single Threaded Execution
这个模式是把多线程变成单线程,多线程在同时访问一个变量时,会发生各类莫名其妙的问题,这个设计模式直接把多线程搞成了单线程,因而安全了,固然性能也就下来了。最简单的实现就是利用synchronized将存在安全隐患的代码块(方法)保护起来。在并发领域有个临界区(criticalsections)的概念,我感受和这个模式是一回事。
2. Immutable Pattern
若是共享变量永远不变,那就多个线程访问就没有任何问题,永远安全。这个模式虽然简单,可是用的好,能解决不少问题。
3. Guarded Suspension Patten
这个模式其实就是等待-通知模型,当线程执行条件不知足时,挂起当前线程(等待),当条件知足时,唤醒全部等待的线程(通知),在Java语言里利用synchronized,wait/notifyAll能够很快实现一个等待通知模型。结城浩将这个模式总结为多线程版的If,我以为很是贴切。
4. Balking
这个模式和上个模式相似,不一样点是当线程执行条件不知足时直接退出,而不是像上个模式那样挂起。这个用法最大的应用场景是多线程版的单例模式,当对象已经建立了(不知足建立对象的条件)就不用再建立对象(退出)。
5. Producer-Consumer
生产者-消费者模式,全世界人都知道。我接触的最多的是一个线程处理IO(如查询数据库),一个(或者多个)线程处理IO数据,这样IO和CPU就都能成分利用起来。若是生产者和消费者都是CPU密集型,再搞生产者-消费者就是本身给本身找麻烦了。
6. Read-Write Lock
读写锁解决的读多写少场景下的性能问题,支持并行读,可是写操做只容许一个线程作。若是写操做很是很是少,而读的并发量很是很是大,这个时候能够考虑使用写时复制(copy on write)技术,我我的以为应该单独把写时复制单独做为一个模式。
7. Thread-Per-Message
就是咱们常常提到的一请求一线程。
8. Worker Thread
一请求一线程的升级版,利用线程池解决线程的频繁建立、销毁致使的性能问题。BIO年代Tomcat就是用的这种模式。
9. Future
当你调用某个耗时的同步方法很心烦,想同时干点别的事情,能够考虑用这个模式,这个模式的本质是个同步变异步的转换器。同步之因此能变异步,本质上是启动了另一个线程,因此这个模式和一请求一线程仍是多少有点关系的。
10. Two-Phase Termination
这个模式能解决优雅地终止线程的需求。
11. Thread-Specific Storage
线程本地存储,避免加锁、解锁开销的利器,C#里面有个支持并发的容器ConcurrentBag就是采用了这个模式,这个星球上最快的数据库链接池HikariCP借鉴了ConcurrentBag的实现,搞了个Java版的,有兴趣的同窗能够参考。
12. Active Object(这个不讲也罢)
这个模式至关于降龙十八掌的最后一掌,综合了前面的设计模式,有点复杂,我的以为借鉴的意义大于参考实现。
最近国人也出过几本相关的书,但整体仍是结城浩这本更能经得住推敲。基于共享内存模型解决并发问题,主要问题就是用好锁,可是用好锁,仍是有难度的,因此后来又有人搞了消息传递模型,这个后面再聊。
基于共享内存模型解决并发问题,主要问题就是用好锁,可是用好锁,仍是有难度的,因此后来又有人搞了消息传递模型。
>>>>
0x10 消息传递模型
共享内存模型难度仍是挺大的,并且你没有办法从理论上证实写的程序是正确的,咱们总一不当心就会写出来个死锁的程序来,每当有了问题,总会有大师出来,因而消息传递(Message-Passing)模型横空出世(发生在上个世纪70年代),消息传递模型有两个重要的分支,一个是Actor模型,一个是CSP模型。
>>>>
0x11 Actor模型
Actor模型由于Erlang声名鹊起,后来又出现了Akka。在Actor模型里面,没有操做系统里所谓进程、线程的概念,一切都是Actor,咱们能够把Actor想象成一个更全能、更好用的线程。
在Actor内部是线性处理(单线程)的,Actor之间以消息方式交互,也就是不容许Actor之间共享数据,没有共享,就无需用锁,这就避免了锁带来的各类反作用。
Actor的建立和new一个对象没有啥区别,很快、很小,不像线程的建立又慢又耗资源;Actor的调度也不像线程会致使操做系统上下文切换(主要是各类寄存器的保存、恢复),因此调度的消耗也很小。
Actor还有一个有点争议的优势,Actor模型更接近现实世界,现实世界也是分布式的、异步的、基于消息的、尤为Actor对于异常(失败)的处理、自愈、监控等都更符合现实世界的逻辑。
可是这个优势改变了编程的思惟习惯,咱们目前大部分编程思惟习惯实际上是和现实世界有不少差别的(这个回头再细说),通常来说,改变咱们思惟习惯的事情,阻力老是超乎咱们的想象。
>>>>
0x12 CSP模型
Golang在语言层面支持CSP模型,CSP模型和Actor模型的一个感官上的区别是在CSP模型里面,生产者(消息发送方)和消费者(消息接收方)是彻底松耦合的,生产者彻底不知道消费者的存在,可是在Actor模型里面,生产者必须知道消费者,不然没办法发送消息。
CSP模型相似于咱们在多线程里面提到的生产者-消费者模型,核心的区别我以为在于CSP模型里面有相似绿色线程(green thread)的东西,绿色线程在Golang里面叫作协程,协程一样是个很是轻量级的调度单元,能够快速建立并且资源占用很低。
Actor在某种程度上须要改变咱们的思惟方式,而CSP模型貌似没有那么大动静,更容易被如今的开发人员接受,都说Golang是工程化的语言,在Actor和CSP的选择上,也能够看到这种体现。
>>>>
0x13 多样世界
除了消息传递模型,还有事件驱动模型、函数式模型。事件驱动模型相似于观察者模式,在Actor模型里面,消息的生产者必须知道消费者才能发送消息,而在事件驱动模型里面,事件的消费者必须知道消息的生产者才能注册事件处理逻辑。
Akka里消费者能够跨网络,事件驱动模型的具体实现如Vertx里,消费者也能够订阅跨网络的事件,从这个角度看,你们都在取长补短。
闲话高并发的那些神话,看京东架构师如何把它拉下神坛