什么是『并发』
在面试中或者别的场景经常会听到有人在“吹逼”说“高并发”(web 开发),那到底什么是一个网站的高并发?乍看上去似乎很容易看懂它指的是什么:同一时间处理大量的请求和服务呗。比如,同一时刻有上万人在用百度搜索内容,那么百度必然是一个超高并发的网站。
这么理解对不对呢?不准确。讲“并发”,还得先退回来先看看什么是“并行”,比较了“并行”和“并发”才能更好的理解。笼统来讲,“并行”是物理上真正的“同时执行”,而“并发”是逻辑上在“同一段时间内”执行多个任务。
理解这个,我们可以再退一步用操作系统的知识来帮助理解(计算机学习越往后越发现操作系统的设计真的是九阳神功)。我们知道 CPU 的执行其实是一根时间线的,就是说它是单线程地在同一时刻只能去做一件事,那为何我们在使用的时候可以同时打开多个应用程序呢?那是因为 CPU 高速的轮训机制,每个任务轮流执行,每次执行时间低到普通人根本感知不到(比如0.01ms),所以逻辑上我们认为在同一段时间内(比如1s)CPU 执行着多个任务。这也是为什么 CPU 越高频(每秒的工作量越多)性能就越强大,就是这个道理。
CPU 的这个工作机制,其实就是“并发”(有没有很诧异),而称 CPU 的这个设计是“高并发的”。
后来,CPU 变成多核了,每个核心都能够隔离地互不干扰地执行自己的任务,这个时候在 A 核执行任务 a 的同一时刻 B 核可以执行任务 b,对于整个 CPU 来说,这就是“并行”,称 CPU 可以“并行”执行。这就是真正意义上的“同时执行”了。
好了,从本质上我们理解了“并发”和“并行”后,回过头在看看网站的“并发”:
同一时间处理大量的请求
该如何理解这句话?
这里的“同一时间”,其实指的是“同一时间段”,而不是我们认为的『同一时刻』。我们在使用网站的时候,感知的或者在意的是浏览器是否能在 3 秒甚至 1 秒内渲染出我需要的页面,当有 10 个人同时访问的时候,服务器在同一时间收到了 10 个请求,如果服务器处理一个请求需要的时间是 100ms,那么服务器就能在 1 秒钟处理完这个 10 个并发访问,逻辑上这个 10 个人就能同时看到页面。这里牵涉到一个概念:QPS,即每秒能够处理的请求数,可以理解为每秒并发访问量。但实际上,从本质来讲服务器仍然是线性处理请求的,因为计算的架构设计本身就是满足并发的,所以并发并不需要 web 开发人员刻意从零去做什么,根据服务器的性能导致 QPS 的不同(当然还有很多其他的原因会影响 QPS,这里为了说明并发而简化了),从而决定网站的并发量。所以,网站的并发,在开发人员什么都不做的前提下就是成立的。
Ningx 的高并发
当前流行的 Nginx 之所以又轻量又高性能(高并发),原因之一就是 Nginx 使用了『进程池 + 单线程』的工作模式。首先,按照物理 CPU 的个数设计进程池的数量,做到真正的并行。其次,在利用 Linux 的 epoll 实现的 I/O 多路复用技术加持下,将所有请求放在一个线程内处理,从而实现无阻塞的并发,最大效率地压榨 CPU(核心)的计算能力。
一个 CPU 对应一个进程是为了防止进程间上下文切换的成本;单线程而不是多线程是为了防止线程间上下文切换的成本;『多进程单线程』而不是『单进程多线程』是为了防止其中一个线程出问题而导致整个服务的崩溃。
通过单线程的原理我们也可以看到,所有(大量)的 web 请求其实是用一个 CPU(核心)高并发处理的,从底层架构和 web 服务器的设计再一次印证了:网站的并发在开发人员什么都不做的前提下就是成立的。
说到这里,『并发』这个原本看起来简单又神秘的概念就已经说清楚了,其实它并没有人们原以为得那么高端。
如何实现『高并发』
其实刚刚已经提到了,服务器的性能一定程度上影响了并发量,所以要提升并发量,很简单,扩容呗,提升 CPU、内存、带宽,用 SSD 硬盘,总之任何能提升服务器本身性能的手段都可以。这种方法最直观、简单、有效,可是,并不推荐。试想,如果你真的要去设计一个需要满足高并发业务的系统架构,一台服务器就够了吗?提升单台服务器的性能就够了吗?单台服务器的性能又能提升到什么程度呢?所以,分布式架构就应运而生了,分布式架构所要解决的问题之一就是高并发,包括搭建负载均衡等等的技术手段,都是为了分散请求利用分布式的架构来提升系统整体的并发量。
但是,最好的方式,还不是这个。高并发的皇冠,是缓存。
要处理大量的请求,最好的方式不是不断提高服务器的性能然后不停的处理请求,而是压根就不处理,让客户端直接获取到访问的结果,简单来说,这就是缓存。能通过本地缓存获取的,决不走网络;能通过 CDN 获取的,决不进服务器;能通过服务器缓存返回的,决不用程序计算;能通过内存读数据的,决不去数据库或硬盘。这一层层的缓存,目的就是尽可能地让客户端在最近的距离直接获取到请求的结果,释放服务器的负载,尤其是静态资源,当上千万人同时访问同一个页面,同一区域的用户可以通过同一个 CDN 的边缘节点直接返回页面数据,从根本上解决了这个业务需求下的高并发问题(CDN 本身的高并发不在本文讨论范围内)。
一致性
然而,在多数情况下,当我们谈到『高并发』时,不可避免地会谈到『一致性』的问题。想象一下购物场景,当库存只有 100 的时候,同时收到 10000 个并发下单请求,如何保证不超卖,其实这种业务场景牵涉到另外两个概念:事务,锁。由于这不是本文的主要内容,就不展开了,但是可以提到的一点是,保证一致性同样可以利用缓存来做,比如每次有修改请求的时候就将对象 id 保存到服务器缓存(比如 redis),当有新的修改请求的时候就去缓存里找一下该对象是否正在被更新,如是则阻止修改或排队等候。这比单纯在数据库层面设计锁的机制更有效,因为这个方法直接避免了数据库的高并发读写(实际上数据库往往是承受不住大量高并发读写请求的),与在服务器前架设缓存从而根本上避免了服务器的高并发请求是一个道理。