乐趣区

它们为什么这么快

一直以来,计算机体现的都是它的工具价值:提高人们的效率。所以,无论是从底层硬件、中间操作系统层还是上层应用软件,速度(包括计算速度和响应速度)快始终是计算机不懈的追求。

快都是相似的,不快有各种各样的原因。

本篇就来聊一聊计算机这相似的『快』,并结合 NginxRedis的案例来详细说明这些『快』的常见『套路』。

分时操作系统

早期计算机资源极为有限(现在也是如此且永远会如此),但是科技的成果不应该被独享,需要为大众服务。于是在 20 世纪 70 年代引入了分时的概念,让计算机可以对它的资源进行按时间片轮转(共享 )的方式供多个用户使用,极大地降低了计算机成本( 经济性),让个人和组织可以在不实际拥有计算机的情况下使用计算机。这种使用分时的方案为用户服务的计算机系统即为分时操作系统。

:分时模型的引入是计算机历史上的一次重大技术革新,Unix 以及类 Unix 操作系统都属于分时操作系统。

多进程与多线程

面对多个用户任务的请求,操作系统自然需要寻找一种优秀的模型来高效地调度它们。用户的任务映射到计算机中就是程序,而程序的执行实例即进程,所以这个优秀的模型就是 进程模型,进程是分时操作系统进行资源分配和调度的基本单位,所有发生的一切均在在此基础上展开。

从用户任务到操作系统进程图

进程模型很好地解决了多任务请求的问题,且 IPC(进程间通信)模型很好地解决了多任务之间的协作问题,但是计算机 资源仍然是有限的 (始终要记住这一点),进程有自己独立的虚拟地址空间、文件描述符以及信号处理等,单个进程运行起来仍然消耗大量的内存和处理器资源,且多进程切换的开销也很大,所以还需要再 共享 / 复用 些什么,于是 线程模型 便应运而生了。

线程是在进程的基础上进一步优化操作系统的调度方式,增大可以共享的粒度。一个进程内的线程除了可以拥有独立的调用栈、寄存器和本地存储,还可以共享当前进程的所有资源。多线程模型可以更大限度地利用 CPU 资源,在当前线程阻塞的时候可以由其他线程获取 CPU 执行权,从而提高系统的响应速度。如果再加上 Processor Affinity(处理器亲和性 / 关联)特性:把任务分配到指定的 CPU Core(核心)上去,这样还可以省去线程切换的开销( 一个 Core 分配一个线程)。

进程 - 线程模型图

如果说一个任务对应一个进程,那么一个任务内的一个部分(子任务)则对应一个线程。如果说 多进程解决 的是 计算问题 (基于多核 CPU),提高了计算性能,那么一个进程中的 多线程 解决 的是 阻塞问题,提升了响应速度。

IO 多路复用

现实场景中,多进程 / 线程可以一定程度上提高计算机执行效率,但是在有限的 Cores 上可以并发的进程 / 线程数会达到一定的瓶颈,即无法规模化增长:进程 / 线程数过少则并发性能不高;进程 / 线程数过多则频繁的上下文切换会带来巨大的时间开销。

例如设计一个网络应用程序,每个连接分配一个进程 / 线程。这种架构简单且容易实现,但是当要同时处理成千上万个连接时,服务器程序则 无法规模化增长 (系统资源会随着连接数增长而逐渐耗尽,Apache 服务器程序就有这个问题)。同时,这也存在一个巨大的资源利用上的不对称:相当轻量级的连接(由文件描述符和少量内存表示) 映射到单独的线程或进程(这是一个非常重量级的操作系统对象)。所以说,一个请求分配一个进程 / 线程的模式虽然容易实现,但它是 对计算机资源极大的浪费。这就是著名的 C10K 问题(即单机支持 1 万个并发连接问题)。

计算机 资源仍然是有限的(始终要记住这一点),如何在有限的计算机资源上让计算机执行的更快呢?优秀的程序员每天都会问计算机一遍:『还可以更快吗』。

所以,优秀的程序员们很快就会想到:是否可以让一个进程 / 线程处理多个连接。从而解决 C10K 问题的良药则是:I/ O 多路复用(从 select 到 poll),对 I / O 异步调用而不会产生阻塞。如通过 select 系统调用,本来由请求线程进行的轮询操作现在改由内核负责,表面上多了一层系统调用的时间,但是由于其支持多路 I /O,故提高了效率。这是 并发的真正关键所在。如果将内核的多路轮询操作改为基于事件通知的方式(即 epoll),epoll 会把哪个描述符发生了怎样的 I / O 事件通知我们,免去了轮询操作,从而进一步提高了并发性能。

上面简单论述了计算机应用程序追求更快执行的历史演进与基本原理,下面来看一下业界在『追求更快』上基于此的最佳实践。

Nginx 为什么这么快

Nginx 是一款业界公认的高性能 Web 和反向代理服务器程序,以高性能与高并发著称,官方测试结果中,其可支持五万个并发连接(在实际场景中,支持 2 万 - 4 万个并发连接)。

Nginx 高并发的因素大致可以归纳为以下几点:

  1. I/ O 多路复用:从 select 到 poll

  2. 事件通知(异步):从 poll 到 epoll

  3. 内存映射文件:从读文件到内存映射文件 

  4. Processor Affinity:一个 Core 指定分配一个进程数

  5. 进程单线程模型:避免了线程切换开销 

  6. 线程池功能(1.7.1+),将可能阻塞的 I / O 模块扔到线程池里去

Redis 为什么这么快

Redis 是一种应用广泛的高并发内存 KV 数据库,其高并发主要由以下几点保证:

  1. I/ O 多路复用:从 select 到 poll

  2. 事件通知(异步):从 poll 到 epoll

  3. 基于内存操作:读写速度非常快

  4. 单线程模式:避免了线程切换开销

  5. 多线程启用:无独有偶,类似 Nginx 的线程池功能,Redis4.0 引入了多线程,专门用于处理一些容易阻塞的大键值对的场景。

总结

通过 Nginx 与 Redis 案例可以看到,设计此类应用程序使其快的『套路』是相似的:

  • 第一步:使用 Processor Affinity 特性:根据 CPU 核心数目确认并发的进程个数,然后将一个进程指定绑定在某一个 CPU 核心上。

  • 第二步:使用进程单线程模式,每个进程 - 线程固定绑定在某个 CPU 核上执行,避免了线程切换带来的开销。且多个 CPU 核上的进程之间并行执行。

  • 第三步:使用 I / O 多路复用,1 个线程处理 N 个连接,这是高并发最为关键的一步。

  • 第四步:使用事件通知,由阻塞改为非阻塞事件驱动,避免了文件描述符的轮询操作。

  • 第五步:针对某些执行时间过长容易阻塞的场景启动线程池功能,进一步提高响应速度

  • 第六步:针对某些具体场景的 tuning,如 Nginx 的内存映射文件

那么,除了这些,还可以更快吗?不远的将来,服务器将要处理数百万的并发连接(C10M 问题)。由于 CMOS 技术方法已经接近物理极限,摩尔定律即将终结,CPU 则向着多核的方向发展。多核 将识别并行性和决定如何利用并行性的责任转移给程序员和语言系统。于是便出现了专门用于并发场景的编程语言,如 Erlang,Golang 等,从一个全新的模型视角去重新审视问题,解决问题,让计算机更快。

本文首发微信公众号:yablog

欢迎扫码关注

退出移动版