原文来自:https://pphc.lvwenhan.com —— 吕文翰

通用设计方法

⾼并发的哲学原理就是——找出单点,进⾏拆分。要将每⼀个“⼤单点”都拆成“一个⼩单点 + 多个资源并⾏”的形式。

高并发系统的通用设计方法是什么?

⾼并发系统遇到性能瓶颈,⼀定是某个单点资源达到了极限,例如数据库、后端云服务器、⽹络带宽等,需要找到这个瓶颈资源,拆分它,提升整个系统的容量。具体来说,有如下⼏个设计⽅向:

  1. 负载均衡:这是解决⾼并发问题的最基本和最直接的⽅式。负载均衡可以将请求分散到多个服务器上,从⽽使得每个服务器的负载保持在⼀个相对较低的⽔平,提⾼了系统的整体处理能⼒。

  2. 缓存技术:缓存是提⾼系统性能的⼀种重要⼿段。通过将经常访问的数据存储在内存中,可以⼤⼤减少对数据库的访问,从⽽提⾼系统的响应速度。

  3. 异步处理:异步处理是⼀种将耗时的操作转化为后台任务进⾏处理的⽅式,这样主线程就可以⽴即返回,不需要等待这些操作完成。这种⽅式可以提⾼系统的并发处理能⼒。

  4. 消息队列:消息队列是⼀种⽤于处理⼤量并发请求的技术。通过将请求放⼊消息队列中,然后由专门的服务进程来处理这些请求,可以避免主线程被阻塞,从⽽提⾼系统的并发处理能⼒。

  5. 数据库优化:对于数据库来说,优化SQL语句、合理设置索引、使⽤数据库集群等都可以提⾼数据库的处理能⼒,从⽽提⾼整个系统的性能。

  6. ⽔平扩展:当系统的负载达到⼀定程度时,可以通过增加更多的服务器来扩展系统的能⼒。这通常需要对系统进⾏微服务化改造,每个服务都可以独⽴地部署和扩展。

  7. 服务隔离:在⾼并发系统中,通常会有多个服务同时运⾏。为了保证服务的独⽴性和稳定性,需要将不同的服务隔离开来,避免⼀个服务的故障影响到其他服务。

高并发系统的拆分顺序是什么样的?

⾼并发系统的拆分顺序应该从静态资源拆分开始,逐步进⾏到数据库和后端计算的机器分离,然后是设计负载均衡和分布式计算架构,接着是利⽤数据库集群和分布式数据库提升数据库性能,最后可以考虑基于地域

进⾏数据库拆分。展开如下:

  1. 静态资源拆分:⾸先,将系统中的静态资源(如图⽚、CSS ⽂件、JavaScript ⽂件等)进⾏拆分。这样可以避免在⾼并发情况下,静态资源的加载成为系统性能瓶颈。可以将静态资源放在专门的 CDN 上,提⾼访问速度。

  2. 数据库和后端计算的机器分离:将负责处理业务逻辑的服务器与负责存储数据的数据库服务器分离。这样可以降低数据库的压⼒,提⾼数据处理速度。同时,可以将不同的业务逻辑部署在不同的服务器上,提⾼系统的可扩展性和容错能⼒。

  3. 设计负载均衡和分布式计算架构:通过负载均衡技术,将⽤户请求分发到多台服务器上进⾏处理。这样可以充分利⽤多台服务器的资源,提⾼系统的处理能⼒。同时,可以采⽤分布式计算架构,利⽤多台服务器同时进⾏服务,进⼀步提⾼系统的并发处理能⼒。

  4. 数据库集群和分布式数据库:为了提升数据库的性能,可以采⽤数据库集群和分布式数据库技术。数据库集群是将多个数据库实例分布在不同的服务器上,通过数据同步和故障转移机制,保证数据库的⾼可⽤性。分布式数据库是将数据分散在多个节点上,每个节点只负责部分数据的存储和查询,从⽽提⾼整个系统的读写性能。

  5. 基于地域进⾏数据库拆分:如果已经是⼀个全国性系统,可以考虑基于地域进⾏数据库拆分。将全国分成⼏个区域,每个区域的机房只为地理位置在本区的⽤户提供热数据。这样可以避免跨区域的数据传输,降低⽹络延迟,提⾼系统响应速度。对于外区的⽤户,可以将请求转发回原来的⼤区,获得终极的系统容量提升。

计算资源高并发

资源隔离

运维的核⼼价值不在于资源的扩充,⽽在于资源的隔离。

由于许多软件都具有类似 Apache 的“单机性能极限”,因此采⽤虚拟化技术将单个机器拆分成多个机器,可以直接实现性能的提升。假设我们将 Apache、Redis、MySQL 和 Elasticsearch 都安装在裸⾦属操作系统上,它们可能会异⼝同声地表⽰:即使有这么多核⼼,我也⽤不上啊

c10k问题,指的是:服务器如何支持10k个并发连接,也就是concurrent 10000 connection(这也是c10k这个名字的由来)。由于硬件成本的大幅度降低和硬件技术的进步,如果一台服务器能够同时服务更多的客户端,那么也就意味着服务每一个客户端的成本大幅度降低。

省略去虚拟化技术介绍

容器

文件系统隔离:Chroot

1979 年,美国计算机科学家 Bill Joy 提出并实现了 Chroot(Change Root)命令,内置到了第七版 Unix系统中,它可以改变某个进程的⽂件系统根⽬录:限制这个进程以及所有⼦进程所能访问的⽂件系统,提升Unix 系统安全性。后来⼈们发现这个⼯具可以很好地应⽤在软件的开发和测试过程中:它可以隔离多个软件之间的冲突和⼲扰,为它们提供相对独⽴的运⾏环境。

但是,作为⼀个安全⼯具,Chroot 还是不够安全,因为除了⽂件系统,软件还是可以通过⼤量的系统调⽤突破限制,这并不是⼀种完美的⽂件系统隔离⽅法。为了解决这个问题,2000 年 Linux Kernel 2.3.41 引⼊了 pivot 使⽤ pivot_root 技术,它通过直接切换“根⽂件系统”的⽅式提⾼了安全性,今天常见的容器技术也都是优先用 pivot_root 来做⽂件系统隔离的。当然,我们需要知道的是,即便到了 2023 年,⽂件系统的隔离依然不是完美的,如果你在 Docker 内运⾏了⼀个未知应⽤,你的系统还是有很⼤的被⿊的风险。

进程访问隔离:Namespaces

命名空间,后端开发者肯定都不陌⽣,他就是 Java 和 Go 中的包名:给类安排⼀个前缀,避免同名的类搞混。但是 Linux 的 Namespaces 却不仅仅是为了防⽌同名混淆。⾃ 2002 的 Kernel 2.4.19 开始,Namespaces 就被引⼊了 Linux 内核,⽽且它在刚刚发布的时候和 Chroot ⼲的事情是⼀样的:隔离⽂件系统。由于 Unix ⼀切皆⽂件的思想,可以把初代 Namespaces 看做⼀种更⾼级更完善的 Chroot。

后来,随着⽤户量的增加,⼤家迫切需要 Namespaces 把其它资源也隔离⼀下,到了 Kernel 5.6 时代,Namespaces 已经具备了⽂件系统、主机名、NIS 域名(⼀种 Unix/Linux 的域控制系统)、进程间通信管道(IPC)、进程编号(PID)、⽹络、⽤户和⽤户组、Cgroup、系统时间⼀共⼋种隔离能⼒。

Namespaces 是容器技术的坚实基础:它不是虚拟机,它没有搞⼀套虚拟的操作系统接⼝,⽽是忠实地把宿主机的信息暴露给了某个 namespace 内的进程,但是不同的 namespace 内的进程之间是相互看不见的。它们都以为是⾃⼰在独占操作系统的全部资源。

下⾯问题来了,软件不都是“有多少资源就占⽤多少资源“的吗?如果某个进程把系统资源吃完了该怎么办呢?轮到系统资源限制功能登场了。

系统资源限制:Cgroups

如果我们想抛弃虚拟机技术,⽤软件的⽅式直接限制某个进程所能消耗的资源上限,那么就需要内核、基础库、系统调⽤以及应⽤软件紧密协同配合。

2008 年,⾕歌贡献的 Cgroups 第⼀次被合并进了 Kernel 2.6.24 中,⾃此,内核第⼀次拥有了完善的“进程资源控制”功能,这可以视为容器技术的第⼀声啼哭。发展到今天,Cgroups 的控制能⼒已经⾮常完善了,它可以限制、记录、隔离⼀个进程组所使⽤的全部物理资源,功能⾮常丰富:

  1. Block I/O(blkio):限制块设备(磁盘、SSD、USB 等)的 I/O 速率上限
  2. CPU Set(cpuset):限制任务能运⾏在哪些 CPU 核⼼上
  3. CPU Accounting(cpuacct):⽣成 Cgroup 中任务使⽤ CPU 的报告
  4. CPU (CPU):限制调度器分配的 CPU 时间
  5. Devices (devices):允许或者拒绝 Cgroup 中任务对设备的访问
  6. Freezer (freezer):挂起或者重启 Cgroup 中的任务
  7. Memory (memory):限制 Cgroup 中任务使⽤内存的量,并⽣成任务当前内存的使⽤情况报告
  8. Network Classifier(net_cls):为 Cgroup 中的报⽂设置上特定的 classid 标志,这样 tc 等⼯具就能根据标记对⽹络进⾏配置
  9. Network Priority (net_prio):对每个⽹络接⼝设置报⽂的优先级
  10. perf_event:识别任务的 Cgroup 成员,可以⽤来做性能分析

应用封装

昙花一现的 LXC

LXC 是第⼀代容器封装技术,也是 Docker 最早期采⽤的技术⽅案,但它虽然也把应⽤封装成了容器,但却是直接封装成的 pod,⼀个 pod 中包含多个软件,⽆法⾃定义。

LXC 就像是新时代的 yum groupinstall 命令,虽然它确实属于容器技术,但是他的管理思维依然停留在“⼀次性安装多个软件”的阶段,价值有限,在 Docker 抛弃它以后,逐渐没落。

Docker:一夜爆红只需要一个好点子

2010 年,⼏个⼤胡⼦年轻⼈创⽴了⼀家名为 dotCloud 的 PaaS(平台即服务)公司,随后便获得了 YC(Y Combinator 风险投资公司)的投资,但是三年过去,随着⾕歌、亚马逊、微软等巨头的⼊场,dotCloud的业务每况愈下。终于,在 2013 年 3 ⽉,他们决定将核⼼技术 Docker 开源,随后的⼀年,Docker ⽕爆全球。

Docker 的爆⽕在于它提出了⼀套恰逢其时的容器概念:① 以应⽤为中⼼ ② 可以派⽣版本 ③镜像可以上传并共享。

Kubernetes——软件定义计算

在详细介绍了容器技术的发展历程和技术特性之后,我们现在正式进⼊容器编排领域。容器技术彻底改变了软件的分发⽅式,它使得软件不再仅仅分发⼆进制⽂件,⽽是直接分发整个虚拟化环境。这种变⾰使得软件交付更加便捷、精确和可控。⽽以 Kubernetes 为代表的容器编排⼯具,则通过软件对分散在世界各地的各种不同硬件配置的物理服务器进⾏逻辑上的“标准化”和“虚拟化”,进⽽将跨数据中⼼甚⾄跨时区的海量服务器的运维从杂乱⽆章的状态转变为全局统⼀协调的状态,让它们百依百顺,如臂指使——容器编排是⼀种更加优秀的基础设施并发技术。

网络资源高并发

概念介绍

负载均衡:负载均衡是⼀种将请求分发到多个服务器的技术,以平衡服务器的负载并提⾼整体性能。常见的负载均衡算法有轮询(Round Robin)、加权轮询(Weighted Round Robin)、最⼩连接数(Least Connections)等。

应⽤⽹关:应⽤⽹关是⼀个位于客户端和服务器之间的中间层,它可以实现请求路由、过滤、认证、限流等功能。应⽤⽹关可以帮助我们更好地管理和维护 Web 服务,灵活、动态地控制流量去往的⽅向。

什么是应用网关

应⽤⽹关,也被称为 API ⽹关,顾名思义,它是所有 API 请求的⼊⼝:它接收所有的HTTP/HTTPS/TCP 请求,并将请求转发给真正的上游服务器。这些上游服务器可以是⼀堆虚拟机、容器,甚⾄是多个数据中⼼各⾃的应⽤⽹关。由于应⽤⽹关所承担的任务相对较少,因此它能够使⽤单台服务器⽀持很⾼的并发量。

常见的应⽤⽹关软件包括 HAProxy、Nginx、Envoy 等,⽽ Cisco、Juniper、F5 等⼀体化设备⼚商也提供相关的硬件产品。除了提升系统容量外,应⽤⽹关还具有许多其他优势。

  1. 解放后端架构 经过对应⽤⽹关三年多的使⽤之后,笔者现在认为所有系统都应该放在应⽤⽹关的背后,包括开发环境。应⽤⽹关对后端架构的解放作⽤实在是太⼤了,可以让你在后端玩出花来:各种语⾔、各种技术、各种部署形式、甚⾄全国各地的机房都可以成为某条 URL 的最终真实服务⽅,让你的后端架构彻底起飞。

  2. TLS 卸载 终端⽤户访问应⽤⽹关时采⽤的是 HTTPS 协议,这个协议需要对数据进⾏加密解密,应⽤⽹关⾮常适合完成这个任务,⽽背后的业务系统只需提供标准 HTTP 协议即可,从⽽降低了业务系统的部署复杂度和资源消耗。

  3. 身份验证和安全性提升 应⽤⽹关可以对后端异构系统进⾏统⼀的⾝份验证,⽆需单独实现。同时也可以统⼀防⽕墙⽩名单,后端系统防⽕墙只需对⽹关 IP 开放,极⼤地提升了后端系统的安全性,降低了海量服务器安全管理的难度。甚⾄可以针对某条 API 进⾏单独鉴权,使系统的安全管控能⼒⼤幅提升。

  4. 指标和数据收集 由于所有流量都会经过⽹关,因此对指标进⾏收集变得简单了。你甚⾄可以将双向流量的内容全部记录下来,⽤于数据统计和安全分析。

  5. 数据压缩与转换 应⽤⽹关还可以统⼀对流量进⾏ gzip 压缩,可以将所有业务⼀次性升级到 HTTP/2 和 HTTP/3,可以对数据进⾏格式转换(XML 到 JSON)和修改(增加/修改/删除字段),总之就是能够灵活应对各种需求,随⼼所欲地操控输⼊和输出的数据。

负载均衡器的工作原理

负载均衡器/⽹关只需要执⾏两个主要任务:建⽴五元组并关联,以及修改数据包的地址和端⼝并将其发送出去。这个过程在⽹络领域被称为 NAT(⽹络地址转换)。

负载均衡器怎样修改数据包

负载均衡器通过修改数据包的源地址、⽬标地址、端⼝号等信息来实现请求的转发和负载均衡。具体的修改⽅式取决于负载均衡器的实现⽅式和⼯作层级。

在⽹络层(第三层)的负载均衡中,如LVS(Linux Virtual Server),数据包修改通常是通过⽹络地址转换(NAT)技术实现的。具体步骤如下:

  1. 通过配置负载均衡器,将其设置为对外公开的⼊⼝,即公⽹ IP 地址。
  2. 当客户端发送请求时,请求⾸先到达负载均衡器。
  3. 负载均衡器根据设定的负载均衡算法选择⼀个后端服务器。
  4. 负载均衡器将客户端请求中的⽬标 IP 地址和端⼝号修改为所选择的后端服务器的 IP 地址和端⼝号,并将修改后的数据包转发给该后端服务器。
  5. 后端服务器接收到转发的数据包后,处理请求并将响应返回给负载均衡器。
  6. 负载均衡器再将响应中的源 IP 地址和端⼝号修改为负载均衡器⾃⾝的 IP 地址和端⼝号,并将响应转发给客户端。

在传输层(第四层)的负载均衡中,如四层负载均衡器,数据包修改⼀般是通过代理⽅式实现的。具体步骤如下:

  1. 客户端发送请求到负载均衡器。
  2. 负载均衡器接收到请求后,建⽴⼀个与客户端的连接,并同时建⽴⼀个与后端服务器的连接。
  3. 负载均衡器将客户端请求中的数据包直接转发给后端服务器。
  4. 后端服务器处理请求并将响应返回给负载均衡器。
  5. 负载均衡器将响应中的数据包直接转发给客户端。

数据库高并发

数据库的单点就单在了磁盘上

集中式存储的优缺点

集中式存储的优点包括:

  1. 部署简单、管理⽅便:所有的数据都存储在单⼀的节点上,只需要管理⼀个设备或服务器。

  2. 数据⼀致性⾼:数据存储在单⼀的节点上,⼀致性⾮常⾼。

  3. 存取速度快:直接通过磁盘硬件接⼝传输数据,不需要跨越⽹络传输数据。 集中式存储的缺点包括:

  4. 单点故障:如果集中式存储的节点出现故障,整个系统将⽆法访问数据,导致服务中断。

  5. 扩展性差:随着数据量的增加,集中式存储的性能可能会变差,难以实现横向扩展。

  6. ⾼成本:为了提⾼性能和可靠性,集中式存储通常需要昂贵的硬件设备和专业的维护⼈员。

分布式存储的优缺点

分布式存储的优点包括:

  1. 可扩展性好:由于数据存储在多个节点上,系统可以根据需要动态添加或删除存储节点,从⽽具有更好的可扩展性。

  2. 容错性强:分布式存储通过复制和分散数据存储来提⾼系统的容错性。如果某个节点发⽣故障,系统可以从其他节点中恢复丢失的数据。

  3. 读性能更好:分布式存储具有更好的性能,因为数据可以并⾏读取,减少了瓶颈和延迟。但是需要注意的是,由于需要数据校验,写性能更差。

分布式存储的缺点包括:

  1. 数据⼀致性:在分布式存储系统中,数据分布在多个节点上,可能导致数据⼀致性问题,需要进⾏额外的⼀致性协议来保证数据的⼀致性。

  2. 写⼊性能差:分布式存储的数据需要在写⼊之后进⾏跨节点校验,需要消耗额外的时间来达成⼀致,所以写⼊性能往往⼤幅落后于单块磁盘。

  3. 复杂性:分布式存储系统的设计和管理相对复杂,需要考虑⽹络、数据分⽚、数据复制等多个⽅⾯的问题。

  4. 延迟:由于数据需要在多个节点之间传输,分布式存储可能会导致较⾼的延迟,尤其是在跨地域的情况下

单机数据库的不可能三角

①持久化 ②事务隔离 ③高性能,三者不可兼得。

为什么不可能

  1. 持久化需要每一次写数据都要落到磁盘上,宕机再启动以后,数据库可以自动修复。如果只要求这一条,很好实现。
  2. 事务隔离需要每一次会话(session)的事务都拥有自己的数据库版本:既要多个并行的事务相互之间不会写到对方的虚拟数据库上(读提交),又要不能读到对方的虚拟数据库上(可重复读),还要在一个事务内不能读到别的事务已经提交的新增的数据(幻读),终极需求则是完全串行化——我的读 session 不结束,你就不能读。这个需求和持久化需求结合以后,会大幅增加日志管理的复杂度,但仍然是可控的。
  3. 读写都要尽量地快:单独实现也很快,内存数据库嘛(如 Redis),但是加上持久化和事务隔离,就很难做了——需要对前两项进行妥协。

MySQL 选择了哪两个

首先,MySQL 选择了持久化:失去人性,失去很多;失去持久化,失去一切。没有持久化能力的核心数据库就做不了核心数据库,这一条是所有磁盘数据库的刚需,完全无法舍弃。

然后,MySQL 选择了一部分高性能:MyISAM 就是为了快速读写而创造的,早期 MySQL 在低配 PC 机上就有不错的性能。后来更高级的 InnoDB 出现了,小数据量时它的读取性能不如 MyISAM,写性能更是彻底拉胯,但是在面对大数据量场景时,读性能非常强,还能提供很多后端程序员梦寐以求的高级功能(例如丰富的索引),承担了大部分互联网核心数据库的角色。

最后,MySQL 将事务隔离拆成了几个级别,任君挑选:你要强事务隔离,性能就差;你能接受弱事务隔离,性能就强。你说无事务隔离?那你用 MySQL 干什么,Redis 它不香吗。

所以 MySQL 其实选择了 持久化1 + 高性能0.8 + 事务隔离*0.5,算下来,还赚了 0.3。

不过,从 MySQL 也可以看出,“数据库的不可能三角”并不是完全互 斥的,是可以相互妥协的。

从读写分离到分布式

由于 Web 系统中读写需求拥有明显的二八分特征——读取流量占 80%,写入流量占 20%,所以如果我们能把读性能拆分到多台机器上,在同样的硬件水平下,数据库总 QPS 也就能提高五倍。

无论是远古时代谷歌的 MMM(Multi-Master Replication Manager for MySQL) 还是中古时代的 MySQL 官方的 MGR(MySQL Group Replication),还是最近刚刚完成开发且收费的官方 InnoDB Cluster,这些主从架构的实现方式都是一致的:基于行同步或者语句同步,近实时地从主节点向从节点同步新增和修改的数据。

由于这种方法必然会让主从之间存在一段时间的延迟(数百毫秒到数秒),所以一般在主从节点前面还要加一个网关进行语句分发,其架构如图 9-2 所示。该集群的运行方式如下:

  1. select等读语句默认发送到从节点,以尽量降低主节点负载
  2. 一旦出现updateinsert等些语句,立刻发送到主节点
  3. 并且,本次会话(session)内的所有后续语句,必须全部发送给主节点,不然就会出现数据写入了但是读不到的情况

写入性能无法提升:由于数据库承载的单点功能实在是太多了(自增、时间先后、事务),导致哪怕架构玩出了花,能写入数据的节点还是只能有一个,所有这些架构都只能提升读性能。

分布式数据库

由于数据库的单点性非常强,所以在谷歌搞出 GFS、MapReduce、Bigtable 三驾马车之前,业界对于高性能数据库的主要解决方案是买 IOE 套件:IBM 小型机 + Oracle 数据库 + EMC 商业存储。而当时的需求也确实更加适合使用商用解决方案。

后来搜索引擎成为了第一代全民网站,而搜索引擎的数据库却“不那么关系型”,所以谷歌搞出了自己的分布式 KV 数据库。后来谷歌发现 SQL 和事务隔离在很多情况下还是刚需,于是在 KV 层之上改了一个强一致支持事务隔离的 Spanner 分布式数据库。而随着云计算的兴起,分布式数据库已经成了云上的“刚需”:业务系统全部上云,总不能还用 Oracle 物理服务器吧?于是云上数据库又开始大踏步发展起来。

第一代分布式数据库:中间件

大名鼎鼎的数据库中间件,其基本原理一句话就能描述:使用一个常驻内存的进程,假装自己是个独立数据库,再提供全局唯一主键、跨分片查询、分布式事务等功能,将背后的多个数据库“包装”成一个逻辑上的单体数据库。

第二代分布式数据库:键值(KV)数据库

数据库技术一共获得过四次图灵奖,后面三次都在关系型数据库领域。事务隔离模型是关系型数据库的核心,非常地简洁、优美、逻辑自恰。

Google 是第一个全民搜索引擎,系统规模也达到了史上最大。但是,搜索引擎技术本身却不需要使用关系型数据库来存储:搜索结果中的网页链接之间是离散的,这已经在前面的第 3 章第 4 节“实战:Go 协程开发高性能爬虫” 中有所体现。

由于搜索不需要关系型数据库,自然谷歌搞的分布式数据库就是使用的 KV 模型。谷歌的三驾马车论文发布以后,业界迅速发展出了一个新的数据库门类 NoSQL(Not Only SQL),专门针对非结构化和半结构化的海量数据。

目前,缓存(Redis)和日志(MongoDB)大家一般都会用 NoSQL 来承载。在这个领域,最成功的莫过于基于 Hadoop 生态中 HDFS 构建的 HBase 了:它主要提供的是行级数据一致性,即 CAP 理论中的 CP,放弃了事务,可以高性能地存储海量数据。

第三代分布式数据库:NewSQL

数据持久性的关键步骤——redo log

写入型 SQL 会在写入缓存页 + 写入磁盘 redo log 之后返回成功,此时,真正的 ibd 磁盘文件并未更新。所以,Spanner 使用 Paxos 协议在多个副本之间同步 redo log,只要 redo log 没问题,多副本数据的最终一致性就没有问题。

事务的两阶段提交

由于分布式场景下写请求需要所有节点都完成才算完成,所以两阶段提交是必须存在的。单机架构下的事务,也是某一旦一条 SQL 执行出错,整个事务都是要回滚的嘛。多机架构下这个需求所需要的成本又大幅增加了,两阶段提交的流程是这样的:

  1. 告诉所有节点更新数据
  2. 收集所有节点的执行结果,如果有一台返回了失败,则再通知所有节点,取消执行该事务

这个简单模型拥有非常恐怖的理论故障概率:一旦在第一步执行成功后某台机器宕机,则集群直接卡死:大量节点会被锁住。

Spanner 使用 Paxos 化解了这个问题:只要 leader 节点的事务执行成功了,即向客户端返回成功,而后续数据的一致性则会基于prepare timestamp(启动时间)和commit timestamp(提交时间)加上 Paxos 算法来保证

多版本并发控制(MVCC)

Spanner 使用时间戳来进行事务之间的 MVCC:为每一次数据的变化分配一个全球统一的时间戳。这么做的本质依然是“空间+时间”换时间,而且是拿过去的时间换现在的时间,特别像支持事后对焦的光场相机。

  1. 传统的单机 MVCC 是基于单机的原子性实现的事务顺序,再实现的事务隔离,属于即时判断。
  2. Spanner 基于 TrueTime 记录下了每行数据的更新时间,增加了每次写入的时间成本,同时也增加了存储空间。
  3. 在进行多节点事务同步时,就不需要再和拥有此行数据的所有节点进行网络通信,只依靠 TrueTime 就可以用 Paxos 算法直接进行数据合并——基于时间戳判断谁前谁后,属于事后判断。
Spanner 放弃了什么

Spanner 是一个强一致的全球数据库,那他放弃了什么呢?这个时候就需要 CAP 理论登场了。

一个分布式系统最多只能同时满足一致性(Consistency)、可用性(Availability)和分区容错性(Partition tolerance)这三项中的两项。

Google Spanner 数据库首先要保证的其实是分区容错性,这是“全球数据库”的基本要求,也最影响他们赚钱;然后是一致性,“强一致”是核心设计目标,也是 Spanner 的核心价值;谷歌放弃的是可用性(A),只有 majority available。

除此之外,为了“外部一致性”,即客户端看到的全局强一致性,谷歌为每一个事务增加了 2 倍的时钟延迟,换句话说就是增加了写操作的返回时间,这就是分布式系统的代价:目前平均 TrueTime 的延迟为 3.5ms,所以对 Spanner 的每一次写操作都需要增加 7ms 的等待时间。

Spanner 一致性的根本来源

大家应该都发现了,其实 Spanner 是通过给 Paxos 分布式共识算法加了一个“本地外挂” TrueTime 实现的海量数据的分布式管理,它实现全局强一致性的根本来源是PaxosTrueTime。而在普通单机房部署的分布式系统中,不需要 GPS 授时和原子钟,直接搞一个时间同步服务就行。

NewSQL 最大的特点就是使用非 B 树磁盘存储结构(一般为 LSM-Tree),在上面构筑一个兼容 SQL 常用语句和事务的兼容层,这样既可以享受大规模 LSM-Tree 集群带来的扩展性和高性能,也可以尽量少改动现有应用代码和开发习惯,把悲伤留给自己了属于是。

目前比较常见的 NewSQL 有 ClustrixDB、NuoDB、VoltDB,国内的 TiDB 和 OceanBase 也属于 NewSQL,但它们俩有本质区别。

第四代分布式数据库:云上数据库

计算与存储分离是一种“低成本”技术

计算与存储分离并不是什么“高性能”技术,而是一种“低成本”技术:关系型数据的存储引擎 InnoDB 本身就是面向低性能的磁盘而设计的,而 CPU 和内存却是越快越好、越大越好,如果还把磁盘和 MySQL 进程部署在同一台物理机内,一定会造成磁盘性能的浪费。计算与存储分离的真正价值在于大幅降低了存储的成本。

计算与存储分离的技术优势

虽然说这个架构的主要价值在于便宜,但是在技术上,它也是有优势的:

它显著降低了传统 MySQL 主从同步的延迟。传统架构下,无论是语句同步还是行同步,都要等到事务提交后,才能开始同步,这就必然带来很长时间的延迟,影响应用代码的编写。而计算和存储分离之后,基于 redo log 传递的主从同步就要快得多了,从 1-2s 降低到了 100ms 以下。由于主从延迟降低,集群中从节点的个数可以提升,总体性能可以达到更高。

CAP 理论

一个分布式系统中,不可能完全满足①一致性、②可用性、③分区容错性。我们以一个两地三中心的数据库为例:

  1. 一致性:同一个时刻发送到三个机房的同一个读请求返回的数据必须一致(强一致读),而且磁盘上的数据也必须在一段时间后变的完全逻辑一致(最终一致)
  2. 可用性:一定比例的机器宕机,其它未宕机服务器必须能够响应客户端的请求(必须是正确格式的成功或失败),这个比例的大小就是可用性级别
  3. 分区容错性:一个集群由于通信故障分裂成两个集群后,不能变成两个数据不一致的集群(脑裂),对外必须依然表现为一个逻辑集群

在一个分布式数据库系统中,到底什么是可以放弃的呢?笔者觉得可以从分布式系统带来了什么优势这个问题开始思考。

相比于单体系统,一个分布式的数据库,在一致性上进步了吗?完全没有。在可用性上进步了吗?进步了很多。在分区容错性上呢?单体系统没有分区,不需要容错。所以,结论已经一目了然了:

①和③都是分布式系统带来的新问题,只有②是进步,那就取长补短,选择牺牲可用性来解决自己引发的一致性和分区容错性两个新问题。这也正是目前常见分布式数据库系统的标准做法。