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

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

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

  具体实现可以参考 Doug Lea 的《Scalable IO in Java》(http://gee.cs.oswego.edu/dl/cpjslides/nio.pdf)。

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

  Reactor 原理图

  Nginx 多进程模型

  Nginx 默认采用的是多进程模型,Nginx 分为 Master 进程和 Worker 进程。

  真正负责监听网络请求并处理请求的只有 Worker 进程,所有的 Worker 进程都监听默认的 80 端口,但是每个请求只会被一个 Worker 进程处理。

  这里面的玄机是:每个进程在 accept 请求前必须争抢一把锁,得到锁的进程才有权处理当前的网络请求。

  每个 Worker 进程只有一个主线程,单线程的好处是无锁处理,无锁处理并发请求,这基本上是高并发场景里面的最高境界了。(参考http://www.dre.vanderbilt.edu/~schmidt/PDF/reactor-siemens.pdf)

  数据经过网卡、操作系统、网络协议中间件(Tomcat、Netty 等)重重关卡,终于到了我们应用开发人员手里,我们如何处理这些高并发的请求呢?我们还是先从提升单机处理能力的角度来思考这个问题。

  突破木桶理论

  我们还是先从提升单机处理能力的角度来思考这个问题,在实际应用的场景中,问题的焦点是如何提高 CPU 的利用率(谁叫它发展的最快呢)。

  木桶理论讲最短的那根板决定水位,那为啥不是提高短板 IO 的利用率,而是去提高 CPU 的利用率呢?

  这个问题的答案是在实际应用中,提高了 CPU 的利用率往往会同时提高 IO 的利用率。

  当然在 IO 利用率已经接近极限的条件下,再提高 CPU 利用率是没有意义的。我们先来看看如何提高 CPU 的利用率,后面再看如何提高 IO 的利用率。

  并行与并发

  提升 CPU 利用率目前主要的方法是利用 CPU 的多核进行并行计算,并行和并发是有区别的。

  在单核 CPU 上,我们可以一边听 MP3,一边 Coding,这个是并发,但不是并行,因为在单核 CPU 的视野,听 MP3 和 Coding 是不可能同时进行的。

  只有在多核时代,才会有并行计算。并行计算这东西太高级,工业化应用的模型主要有两种,一种是共享内存模型,另外一种是消息传递模型。

  多线程设计模式

  对于共享内存模型,其原理基本都来自大师 Dijkstra 在半个世纪前(1965)的一篇论文《Cooperating sequential processes》。

  这篇论文提出了大名鼎鼎的概念信号量,Java 里面用于线程同步的 wait/notify 也是信号量的一种实现。

  大师的东西看不懂,学不会也不用觉得丢人,毕竟大师的嫡传子弟也没几个。

  东洋有个叫结城浩的总结了一下多线程编程的经验,写了本书叫《JAVA多线程设计模式》,这个还是挺接地气(能看懂)的,下面简单介绍一下。

  Single Threaded Execution

  这个模式是把多线程变成单线程,多线程在同时访问一个变量时,会发生各种莫名其妙的问题,这个设计模式直接把多线程搞成了单线程,于是安全了,当然性能也就下来了。

  最简单的实现就是利用 synchronized 将存在安全隐患的代码块(方法)保护起来。

  在并发领域有个临界区(criticalsections)的概念,我感觉和这个模式是一回事。

  Immutable Pattern

  如果共享变量永远不变,那多个线程访问就没有任何问题,永远安全。这个模式虽然简单,但是用的好,能解决很多问题。

  Guarded Suspension Patten

  这个模式其实就是等待-通知模型,当线程执行条件不满足时,挂起当前线程(等待);当条件满足时,唤醒所有等待的线程(通知),在 Java 语言里利用 synchronized,wait/notifyAll 可以很快实现一个等待通知模型。

  结城浩将这个模式总结为多线程版的 If,我觉得非常贴切。

  Balking

  这个模式和上个模式类似,不同点是当线程执行条件不满足时直接退出,而不是像上个模式那样挂起。

  这个用法最大的应用场景是多线程版的单例模式,当对象已经创建了(不满足创建对象的条件)就不用再创建对象(退出)。

  Producer-Consumer

  生产者-消费者模式,全世界人都知道。我接触的最多的是一个线程处理 IO(如查询数据库),一个(或者多个)线程处理 IO 数据,这样 IO 和 CPU 就都能充分利用起来。

  如果生产者和消费者都是 CPU 密集型,再搞生产者-消费者就是自己给自己找麻烦了。

  Read-Write Lock

  读写锁解决的是读多写少场景下的性能问题,支持并行读,但是写操作只允许一个线程做。

  如果写操作非常非常少,而读的并发量非常非常大,这个时候可以考虑使用写时复制(copy on write)技术,我个人觉得应该单独把写时复制作为一个模式。

  Thread-Per-Message

  就是我们经常提到的一请求一线程。

  Worker Thread

  一请求一线程的升级版,利用线程池解决线程的频繁创建、销毁导致的性能问题。BIO 年代 Tomcat 就是用的这种模式。

  Future

  当你调用某个耗时的同步方法很心烦,想同时干点别的事情,可以考虑用这个模式,这个模式的本质是个同步变异步的转换器。

  同步之所以能变异步,本质上是启动了另外一个线程,所以这个模式和一请求一线程还是多少有点关系的。

  Two-Phase Termination

  这个模式能解决优雅地终止线程的需求。

  Thread-Specific Storage

  线程本地存储,避免加锁、解锁开销的利器,C# 里面有个支持并发的容器 ConcurrentBag 就是采用了这个模式。

  这个星球上最快的数据库连接池 HikariCP 借鉴了 ConcurrentBag 的实现,搞了个 Java 版的,有兴趣的同学可以参考。

  Active Object(这个不讲也罢)

  这个模式相当于降龙十八掌的最后一掌,综合了前面的设计模式,有点复杂,个人觉得借鉴的意义大于参考实现。

  最近国人也出过几本相关的书,但总体还是结城浩这本更能经得住推敲。基于共享内存模型解决并发问题,主要问题就是用好锁。

  但是用好锁,还是有难度的,所以后来又有人搞了消息传递模型。

  消息传递模型

  共享内存模型难度还是挺大的,而且你没有办法从理论上证明写的程序是正确的,我们总一不小心就会写出来个死锁的程序来,每当有了问题,总会有大师出来。

  于是消息传递(Message-Passing)模型横空出世(发生在上个世纪 70 年代),消息传递模型有两个重要的分支,一个是 Actor 模型,一个是 CSP 模型。

  Actor 模型

  Actor 模型因为 Erlang 声名鹊起,后来又出现了 Akka。在 Actor 模型里面,没有操作系统里所谓进程、线程的概念,一切都是 Actor,我们可以把 Actor 想象成一个更全能、更好用的线程。

  在 Actor 内部是线性处理(单线程)的,Actor 之间以消息方式交互,也就是不允许 Actor 之间共享数据。没有共享,就无需用锁,这就避免了锁带来的各种副作用。

  Actor 的创建和 new 一个对象没有啥区别,很快、很小,不像线程的创建又慢又耗资源。

  Actor 的调度也不像线程会导致操作系统上下文切换(主要是各种寄存器的保存、恢复),所以调度的消耗也很小。

  Actor 还有一个有点争议的优点,Actor 模型更接近现实世界,现实世界也是分布式的、异步的、基于消息的、尤其 Actor 对于异常(失败)的处理、自愈、监控等都更符合现实世界的逻辑。

  但是这个优点改变了编程的思维习惯,我们目前大部分编程思维习惯其实是和现实世界有很多差异的。一般来讲,改变我们思维习惯的事情,阻力总是超乎我们的想象。

  CSP 模型

  Golang 在语言层面支持 CSP 模型,CSP 模型和 Actor 模型的一个感官上的区别是在 CSP 模型里面,生产者(消息发送方)和消费者(消息接收方)是完全松耦合的,生产者完全不知道消费者的存在。

  但是在 Actor 模型里面,生产者必须知道消费者,否则没办法发送消息。

  CSP 模型类似于我们在多线程里面提到的生产者-消费者模型,核心的区别我觉得在于 CSP 模型里面有类似绿色线程(green thread)的东西。

  绿色线程在 Golang 里面叫做协程,协程同样是个非常轻量级的调度单元,可以快速创建而且资源占用很低。

  Actor 在某种程度上需要改变我们的思维方式,而 CSP 模型貌似没有那么大动静,更容易被现在的开发人员接受,都说 Golang 是工程化的语言,在 Actor 和 CSP 的选择上,也可以看到这种体现。

  多样世界

  除了消息传递模型,还有事件驱动模型、函数式模型。事件驱动模型类似于观察者模式,在 Actor 模型里面,消息的生产者必须知道消费者才能发送消息、

  而在事件驱动模型里面,事件的消费者必须知道消息的生产者才能注册事件处理逻辑。

  Akka 里消费者可以跨网络,事件驱动模型的具体实现如 Vertx 里,消费者也可以订阅跨网络的事件,从这个角度看,大家都在取长补短。

(编辑:济南站长网)

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

热点阅读