Dunwu Blog

大道至简,知易行难

分布式一致性

ACID 理论

ACID 是数据库事务正确执行的四个基本要素。

  • 原子性(Atomicity)
    • 事务被视为不可分割的最小单元,事务中的所有操作要么全部提交成功,要么全部失败回滚。
    • 回滚可以用日志来实现,日志记录着事务所执行的修改操作,在回滚时反向执行这些修改操作即可。
  • 一致性(Consistency)
    • 数据库在事务执行前后都保持一致性状态。
    • 在一致性状态下,所有事务对一个数据的读取结果都是相同的。
  • 隔离性(Isolation)
    • 一个事务所做的修改在最终提交以前,对其它事务是不可见的。
  • 持久性(Durability)
    • 一旦事务提交,则其所做的修改将会永远保存到数据库中。即使系统发生崩溃,事务执行的结果也不能丢失。
    • 可以通过数据库备份和恢复来实现,在系统发生奔溃时,使用备份的数据库进行数据恢复。

一个支持事务(Transaction)中的数据库系统,必需要具有这四种特性,否则在事务过程(Transaction processing)当中无法保证数据的正确性,交易过程极可能达不到交易。

  • 只有满足一致性,事务的执行结果才是正确的。
  • 在无并发的情况下,事务串行执行,隔离性一定能够满足。此时只要能满足原子性,就一定能满足一致性。
  • 在并发的情况下,多个事务并行执行,事务不仅要满足原子性,还需要满足隔离性,才能满足一致性。
  • 事务满足持久化是为了能应对系统崩溃的情况。

本地事务和分布式事务

学习分布式之前,先了解一下本地事务的概念。

事务简单来说:一个会话中所进行所有的操作,要么同时成功,要么同时失败

事务指的是满足 ACID 特性的一组操作,可以通过 Commit 提交一个事务,也可以使用 Rollback 进行回滚。

分布式事务指的是事务操作跨越多个节点,并且要求满足事务的 ACID 特性

分布式事务相比于单机事务,实现复杂度要高很多,主要是因为其存在以下难点

  • 事务的原子性:事务操作跨不同节点,当多个节点某一节点操作失败时,需要保证多节点操作的都做或都不做(All or Nothing)的原子性。
  • 事务的一致性:当发生网络传输故障或者节点故障,节点间数据复制通道中断,在进行事务操作时需要保证数据一致性,保证事务的任何操作都不会使得数据违反数据库定义的约束、触发器等规则。
  • 事务的隔离性:事务隔离性的本质就是如何正确多个并发事务的处理的读写冲突和写写冲突,因为在分布式事务控制中,可能会出现提交不同步的现象,这个时候就有可能出现“部分已经提交”的事务。此时并发应用访问数据如果没有加以控制,有可能出现“脏读”问题。

在分布式领域,要实现强一致性,代价非常高昂。因此,有人基于 CAP 理论以及 BASE 理论,有人就提出了柔性事务的概念。柔性事务是指:在不影响系统整体可用性的情况下(Basically Available 基本可用),允许系统存在数据不一致的中间状态(Soft State 软状态),在经过数据同步的延时之后,最终数据能够达到一致。并不是完全放弃了 ACID,而是通过放宽一致性要求,借助本地事务来实现最终分布式事务一致性的同时也保证系统的吞吐

CAP 理论

CAP 定理是加州大学计算机科学家埃里克·布鲁尔提出来的猜想,后来被证明成为分布式计算领域公认的定理。

CAP 定理,指的是:在一个分布式系统中, 最多只能同时满足其中两项

CAP 就是取 Consistency、Availability、Partition Tolerance 的首字母而命名。

img

  • 一致性(Consistency):在任何给定时间,网络中的所有节点都具有完全相同(最近)的值。
  • 可用性(Availability):对网络的每个请求都会收到响应,但不能保证返回的数据是最新的。
  • 分区容错性(Partition Tolerance):即使任意数量的节点出现故障,网络仍会继续运行。

一致性

一致性(Consistency)指的是多个数据副本是否能保持一致的特性。

在一致性的条件下,分布式系统在执行写操作成功后,如果所有用户都能够读取到最新的值,该系统就被认为具有强一致性。

数据一致性又可以分为以下几点:

  • 强一致性 - 数据更新操作结果和操作响应总是一致的,即操作响应通知更新失败,那么数据一定没有被更新,而不是处于不确定状态。
  • 最终一致性 - 即物理存储的数据可能是不一致的,终端用户访问到的数据可能也是不一致的,但系统经过一段时间的自我修复和修正,数据最终会达到一致。

举例来说,某条记录是 v0,用户向 G1 发起一个写操作,将其改为 v1。

img

接下来,用户的读操作就会得到 v1。这就叫一致性。

img

问题是,用户有可能向 G2 发起读操作,由于 G2 的值没有发生变化,因此返回的是 v0。G1 和 G2 读操作的结果不一致,这就不满足一致性了。

img

为了让 G2 也能变为 v1,就要在 G1 写操作的时候,让 G1 向 G2 发送一条消息,要求 G2 也改成 v1。

img

这样的话,用户向 G2 发起读操作,也能得到 v1。

img

可用性

可用性指分布式系统在面对各种异常时可以提供正常服务的能力,可以用系统可用时间占总时间的比值来衡量,4 个 9 的可用性表示系统 99.99% 的时间是可用的。

在可用性条件下,系统提供的服务一直处于可用的状态,对于用户的每一个操作请求总是能够在有限的时间内返回结果。

分区容错性

分区容错性(Partition Tolerance)指 分布式系统在遇到任何网络分区故障的时候,仍然需要能对外提供一致性和可用性的服务,除非是整个网络环境都发生了故障

在一个分布式系统里面,节点组成的网络本来应该是连通的。然而可能因为一些故障,使得有些节点之间不连通了,整个网络就分成了几块区域。数据就散布在了这些不连通的区域中,这就叫分区。

假设,某个数据项只在一个节点中保存,那么分区出现后,和这个节点不连通的部分就访问不到这个数据了。这时分区就是无法容忍的。

提高分区容错性的办法就是一个数据项复制到多个节点上,那么出现分区之后,这一数据项就可能分布到各个区里。容错性就提高了。

然而,要把数据复制到多个节点,就会带来一致性的问题,就是多个节点上面的数据可能是不一致的。要保证一致,每次写操作就都要等待全部节点写成功,而这等待又会带来可用性的问题。

总的来说就是,数据存在的节点越多,分区容错性越高,但要复制更新的数据就越多,一致性就越难保证。为了保证一致性,更新所有节点数据所需要的时间就越长,可用性就会降低。

大多数分布式系统都分布在多个子网络,每个子网络就叫做一个区(Partition)。分区容错的意思是,区间通信可能失败。比如,一台服务器放在中国,另一台服务器放在美国,这就是两个区,它们之间可能无法通信。

img

上图中,G1 和 G2 是两台跨区的服务器。G1 向 G2 发送一条消息,G2 可能无法收到。系统设计的时候,必须考虑到这种情况。

一般来说,分区容错无法避免,因此可以认为 CAP 的 P 总是成立。CAP 定理告诉我们,剩下的 C 和 A 无法同时做到。

AP or CP

在分布式系统中,分区容错性必不可少,因为需要总是假设网络是不可靠的。因此,CAP 理论实际在是要在可用性和一致性之间做权衡

由于分布式数据存储(如区块链)的性质,分区容错性是一个既定的事实;网络中总会有失败/无法访问的节点(尤其是因为互联网的不稳定特性)。 CAP 定理指出,当存在 P(分区)时,必须在 C(一致性)或 A(可用性)之间进行选择。

(1)AP 模式

AP 模式:对网络的每个请求都会收到响应,即使网络由于网络分区故障而无法保证它是最新的。

选择 AP 模式,实现了服务的高可用。用户访问系统的时候,都能得到响应数据,不会出现响应错误;但是,当出现分区故障时,相同的读操作,访问不同的节点,得到响应数据可能不一样。

(2)CP 模式

CP 模式:如果由于网络分区(故障节点)而无法保证特定信息是最新的,则系统将返回错误或超时。

选择 CP 模式,这样能够提供一部分的可用性。采用 CP 模型的分布式系统,一旦因为消息丢失、延迟过高发生了网络分区,就影响用户的体验和业务的可用性。因为为了防止数据不一致,集群将拒绝新数据的写入。

BASE 理论

什么是 BASE 定理

BASE 定理是对 CAP 中一致性和可用性权衡的结果

BASE 是 基本可用(Basically Available)软状态(Soft State)最终一致性(Eventually Consistent) 三个短语的缩写。

BASE 理论的核心思想是:即使无法做到强一致性,但每个应用都可以根据自身业务特点,采用适当的方式来使系统达到最终一致性。

  • 基本可用(Basically Available)分布式系统在出现故障的时候,保证核心可用,允许损失部分可用性。例如,电商在做促销时,为了保证购物系统的稳定性,部分消费者可能会被引导到一个降级的页面。
  • 软状态(Soft State)指允许系统中的数据存在中间状态,并认为该中间状态不会影响系统整体可用性,即允许系统不同节点的数据副本之间进行同步的过程存在延时
  • 最终一致性(Eventually Consistent)强调的是系统中所有的数据副本,在经过一段时间的同步后,最终能达到一致的状态

img

BASE vs. ACID

BASE 的理论的核心思想是:即使无法做到强一致性,但每个应用都可以根据自身业务特点,采用适当的方式来使系统达到最终一致性。

ACID 要求强一致性,通常运用在传统的数据库系统上。而 BASE 要求最终一致性,通过牺牲强一致性来达到可用性,通常运用在大型分布式系统中。

在实际的分布式场景中,不同业务单元和组件对一致性的要求是不同的,因此 ACID 和 BASE 往往会结合在一起使用。

参考资料

拜占庭将军问题

拜占庭将军问题是由莱斯利·兰波特在其同名论文中提出的分布式对等网络通信容错问题。其实是借拜占庭将军的例子,抛出了分布式共识性问题,并探讨和论证了解决的方法。

分布式计算中,不同的节点通过通讯交换信息达成共识而按照同一套协作策略行动。但有时候,系统中的节点可能出错而发送错误的信息,用于传递信息的通讯网络也可能导致信息损坏,使得网络中不同的成员关于全体协作的策略得出不同结论,从而破坏系统一致性。拜占庭将军问题被认为是容错性问题中最难的问题类型之一。

问题描述

一群拜占庭将军各领一支军队共同围困一座城市。

为了简化问题,军队的行动策略只有两种:进攻(Attack,后面简称 A)或 撤退(Retreat,后面简称 R)。如果这些军队不是统一进攻或撤退,就可能因兵力不足导致失败。因此,将军们通过投票来达成一致策略:同进或同退

因为将军们分别在城市的不同方位,所以他们只能通过信使互相联系。在投票过程中,每位将军都将自己的投票信息(A 或 R)通知其他所有将军,这样一来每位将军根据自己的投票和其他所有将军送来的信息就可以分析出共同的投票结果而决定行动策略。

这个抽象模型的问题在于:将军中可能存在叛徒,他们不仅会发出误导性投票,还可能选择性地发送投票信息

由于将军之间需要通过信使通讯,叛变将军可能通过伪造信件来以其他将军的身份发送假投票。而即使在保证所有将军忠诚的情况下,也不能排除信使被敌人截杀,甚至被敌人间谍替换等情况。因此很难通过保证人员可靠性及通讯可靠性来解决问题。

假使那些忠诚(或是没有出错)的将军仍然能通过多数决定来决定他们的战略,便称达到了拜占庭容错。在此,票都会有一个默认值,若消息(票)没有被收到,则使用此默认值来投票。

上述的故事可以映射到分布式系统中,_将军代表分布式系统中的节点;信使代表通信系统;叛徒代表故障或异常_。

img

问题分析

兰伯特针对拜占庭将军问题,给出了两个解决方案:口头协议和书面协议。

本文介绍一下口头协议。

在口头协议中,拜占庭将军问题被简化为将军 - 副官模型,其核心规则如下:

  • 忠诚的副官遵守同一命令。
  • 若将军是忠诚的,所有忠诚的副官都执行他的命令。
  • 如果叛徒人数为 m,将军人数不能少于 3m + 1 ,那么拜占庭将军问题就能解决了。——关于这个公式,可以不必深究,如果对推导过程感兴趣,可以参考论文。

示例一、叛徒人数为 1,将军人数为 3

这个示例中,将军人数不满足 3m + 1,无法保证忠诚的副官都执行将军的命令。

示例二、叛徒人数为 1,将军人数为 4

这个示例中,将军人数满足 3m + 1,无论是副官中有叛徒,还是将军是叛徒,都能保证忠诚的副官执行将军的命令。

参考资料

如何设计系统

系统设计过程

步骤一、约束和用例

对于任何系统设计,第一件应该做的事是:阐明系统的约束并确定系统需要满足哪些用例。

永远不要假设没有明确说明的事情。一定要尽力收集、理解需求,并设计一个很好地涵盖这些要求的解决方案。

例如,URL 缩短服务可能只为几千个用户提供服务,但每个用户都可能共享数百万个 URL。它可能旨在处理对缩短的 URL 的数百万次点击或数十次点击。该服务可能必须提供有关每个缩短的 URL 的大量统计信息(这会增加您的数据大小),或者可能根本不需要统计信息。

您还必须考虑预期会发生的用例。您的系统将根据其预期功能进行设计。不要忘记确保你知道面试官一开始没有告诉你的所有要求。

步骤二、顶层设计

一旦确定了要设计的系统的范围,接下来就要做顶层设计:概述系统架构中所需的所有重要组件。

此时,应该绘制出主要组件以及它们之间的连接。通常,这种顶层设计是基于主流技术的组合。这就要求设计必须熟悉这些技术,了解其利弊以及适合使用的场景。

步骤三、分析瓶颈

顶层设计很可能会遇到一个或多个瓶颈。这完全没问题,不要指望一个新系统可以立即处理世界上的所有负载。它只需要可扩展,以便您能够使用一些标准工具和技术对其进行改进。

现在有了顶层设计,就要考虑这些组件在系统扩展时面临的瓶颈。也许,系统需要一个负载均衡器和集群来处理用户请求。或者,由于数据容量庞大,以至于需要将数据库分库分表(分布在多台机器上)。这些方案有什么利弊,是否适用?数据库是否太慢,是否需要一些内存缓存?

通常每个解决方案都是某种权衡和取舍。改变某事会使其他事情恶化。然而,重要的是能够讨论这些权衡,并根据定义的约束和用例来衡量它们对系统的影响。

一旦分析清楚核心瓶颈,就可以着手在下一步中去解决它们。

步骤四、扩展设计

首先,你需要了解以下技术手段:

  • 垂直扩展
  • 水平罗占
  • 缓存
  • 负载均衡
  • 数据库复制
  • 数据库分区
  • 异步
  • NoSql

在系统设计方面,回顾现实中的架构非常有用。注意使用了哪些技术。继续研究每一项新技术,看看它解决了什么问题,它的替代品是什么,它擅长的地方,以及失败的地方。

一切都是权衡的结果——这是系统设计中最基本的概念之一。

一些推荐的学习资料

  • 生产中的深度学习:关于 EyeEm 如何构建在大量图像上运行多个深度学习模型的生产系统的精彩故事
  • Uber:一篇关于 Uber 如何快速扩展的好文章,关于将您的服务分解为分布在许多存储库中的许多微服务。
  • Facebook:Facebook 如何在直播中同时处理 800,000 名观众
  • Kraken.io:如何大规模缩放图像优化,本文将更详细地看一些具体使用的硬件方案,以及部署、监控等重要方面
  • Twitter:Twitter 如何处理每秒 3,000 张图片上传以及为什么它使用的旧方式现在行不通
  • 最后,Twitter 子组件的一些很好的例子:存储数据(video | text)和时间轴(video | text)。
  • 有关更高级的示例,请查看 Google、Youtube(video | text)、TumblrStackOverflowDatashift 上的这些帖子。

参考资料

权限认证综述

认证

认证是指根据声明者所特有的识别信息,确认声明者的身份。认证在英文中对应于 identification 这个单词。

最常见的认证实现方式是通过用户名和密码,但认证方式不限于此。下面都是当前常见到的认证技术:

  • 身份证
  • 用户名和密码认证
  • 用户手机认证:手机短信、手机二维码扫描、手势密码
  • 用户邮箱认证
  • 基于时间序列和用户相关的一次性口令
  • 用户的生物学特征认证:指纹、语音、眼睛虹膜
  • 用户的大数据识别认证
  • 等等

为了确认用户的身份,防止伪造,在安全要求高的场合,经常会使用组合认证(或者叫多因素认证),也就是同时使用多个认证方式对用户的身份进行校验。

授权

简单来说,授权一般是指获取用户的委派权限。在英文中对应于 authorization 这个单词。

在信息安全领域,授权是指资源所有者委派执行者,赋予执行者指定范围的资源操作权限,以便执行者代理执行对资源的相关操作。这里面包含有如下四个重要概念,

  • 资源所有者:拥有资源的所有权利,一般就是资源的拥有者。
  • 资源执行者:被委派去执行资源的相关操作。
  • 操作权限:可以对资源进行的某种操作。
  • 资源:有价值的信息或数据等,受到安全保护。

需要说明的是,资源所有者和执行者可以是自然人,就是普通用户,但不限于自然人。在信息安全领域,资源所有者和执行者,很多时候是应用程序或者机器。比如用户在浏览器上登录一个网站,那么这个浏览器就成为一个执行者,它在用户登录后获取了用户的授权,代表着用户执行各种指令,进行购物、下单、付钱、转账等等操作。

同时,资源所有者和执行者可以是分开的不同实体,也可以是同一个。若是分开的两者,则资源执行者是以资源所有者的代理形式而存在。

授权的实现方式非常多也很广泛,我们常见的银行卡、门禁卡、钥匙、公证书,这些都是现实生活中授权的实现方式。其实现方式主要通过一个共信的媒介完成,这个媒介不可被篡改,不可随意伪造,很多时候需要受保护,防止被窃取。

在互联网应用开发领域,授权所用到的授信媒介主要包括如下几种,

  • 通过 web 服务器的 session 机制,一个访问会话保持着用户的授权信息
  • 通过 web 浏览器的 cookie 机制,一个网站的 cookie 保持着用户的授权信息
  • 颁发授权令牌(token),一个合法有效的令牌中保持着用户的授权信息

前面两者常见于 web 开发,需要有浏览器的支持。

鉴权

鉴权是指对于一个声明者所声明的身份权利,对其所声明的真实性进行鉴别确认的过程。在英文中对应于 authentication 这个单词。

鉴权主要是对声明者所声明的真实性进行校验。若从授权出发,则会更加容易理解鉴权。授权和鉴权是两个上下游相匹配的关系,先授权,后鉴权。授权和鉴权两个词中的“权”,是同一个概念,就是所委派的权利,在实现上即为授信媒介的表达形式。

因此,鉴权的实现方式是和授权方式有一一对应关系。对授权所颁发授信媒介进行解析,确认其真实性。下面是鉴权的一些实现方式,

  • 门禁卡:通过门禁卡识别器
  • 钥匙:通过相匹配的锁
  • 银行卡:通过银行卡识别器
  • 互联网 web 开发领域的 session/cookie/token:校验 session/cookie/token 的合法性和有效性

鉴权是一个承上启下的一个环节,上游它接受授权的输出,校验其真实性后,然后获取权限(permission),这个将会为下一步的权限控制做好准备。

权限控制

权限控制是指对可执行的各种操作组合配置为权限列表,然后根据执行者的权限,若其操作在权限范围内,则允许执行,否则禁止。权限控制在英文中对应于 access/permission control。

对于权限控制,可以分为两部分进行理解:一个是权限,另一个是控制。权限是抽象的逻辑概念,而控制是具体的实现方式。

先看权限(Permission),这是一个抽象的概念,一般预先定义和配置好,以便控制的具体实现。权限的定义,若简单点,可以直接对应于一个可执行的操作集合。而一般情况下,会有基于角色的方式来定义权限,由角色来封装可执行的操作集合。

若以门禁卡的权限实现为例,上述两种定义方式则可以各自表达为,

  • 这是一个门禁卡,拥有开公司所有的门的权限
  • 这是一个门禁卡,拥有管理员角色的权限,因而可以开公司所有的门

可以看到,权限作为一个抽象的概念,将执行者和可具体执行的操作相分离。

在上文的讨论中,鉴权的输出是权限(Permission)。一旦有了权限,便知道了可执行的操作,接下来就是控制的事情了。

对于控制,是根据执行者的权限,对其所执行的操作进行判断,决定允许或禁止当前操作的执行。现实生活中控制的实现方式,多种多样,

  • 门禁:控制门的开关
  • 自行车锁:控制车轮
  • 互联网 web 后端服务:控制接口访问,允许或拒绝访问请求

认证和鉴权

认证、授权、鉴权和权限控制这四个环节是一个前后依次发生、上下游的关系,

认证–>授权–>鉴权–>权限控制

需要说明的是,这四个环节在有些时候会同时发生。 例如在下面的几个场景,

  • 使用门禁卡开门:认证、授权、鉴权、权限控制四个环节一气呵成,在瞬间同时发生
  • 用户的网站登录:用户在使用用户名和密码进行登录时,认证和授权两个环节一同完成,而鉴权和权限控制则发生在后续的请求访问中,比如在选购物品或支付时。

无论怎样,若从时间顺序方面来看,这四个环节是按时间前后、依次相继发生的关系。

认证和鉴权的关系:

这两个概念在很多时候是被混淆最多的概念。被混淆的主要原因,如上文所述,很多时候认证、授权、鉴权和权限控制一同发生,以至于被误解为,认证就是鉴权,鉴权就是认证。

其实两者是不一样的概念,两者都有对身份的确认过程,但是两者的主要区别在于,

  • 认证是确认声明者的本身身份,其作为授权的上游衔接而存在
  • 鉴权是对声明者所声明的真实性进行确认的过程,其作为授权的下游衔接而存在

Cinchcast 的架构

Cinchcast 提供的解决方案允许公司创建、共享、衡量和货币化音频内容,以接触和吸引对其业务最重要的人。我们的技术将会议桥接器与实时音频流相结合,以简化在线活动并增强参与者的参与度。 Cinchcast 技术还用于为全球最大的音频社交网络 Blogtalkradio 提供动力。今天,我们的平台每天制作和分发超过 1,500 小时的原创内容。在本文中,我们描述了我们为扩展平台以支持这种规模的数据而做出的工程决策。

统计数据

  • 浏览量每月超过 5000 万
  • 创建了 50000 小时的音频内容
  • 1500 万个流媒体
  • 175,000,000 次广告展示
  • 峰值每秒 40000 并发请求
  • MSSQL、Redis、ElasticSearch 集群中存储的数据达到每天数 TB,
  • 10 人工程师团队
  • 生产环境大概有 100 左右的硬件节点

数据中心

线上网站部署在布鲁克林的数据中心。但 QA 和 Staging 环境则使用了 Amazon EC2 云实例。

——考虑到数据安全,大部分公司不愿意把真实数据部署在云端。

硬件

  • 大概有 50 台 Web 服务器
  • 15 台 MS SQL 数据库服务器
  • 2 台 Redis 的 NoSQL 的键值服务器
  • 2 台 NodeJS 服务器
  • 2 台 弹性搜索集群服务器

开发工具

  • NET 4 C#:ASP.NET 和 MVC3
  • IDE 用的是 Visual Studio 2010 Team Suite
  • 用 StyleCop、ReSharper 来强化代码标准
  • 使用敏捷。其中大的功能用 Scrum,小任务则通过看板任务墙管理
  • 测试和持续集成使用 Jenkins + Nunit
  • 自动化测试则是 Selenium 和 Sauce On Demand

软件和使用的技术

  • Windows Server 2008 R2 的 64 位操作系统
  • 基于微软 Windows Server 2008 Web 服务器下运行的 SQL Server 2005
  • 负载均衡是 EQL(Equalizer load balancers)
  • Redis 作为分布式缓存层和消息分发队列
  • NodeJS 用来进行实时分析和更新仪表盘
  • 搜索用得是 ElasticSearch,日志分析是通过 Sawmill+自定义分析器脚本

监测

  • NewRelic:性能监控
  • 性能对 KPI(转换率,页面浏览量)的影响:Chartbeat:
  • Gomez,WhatsupGold,Nagios 等用来各种预警和报警
  • SQL Server monitoring 的监控:来自 Red Gate 的 SQL Monitor

我们的原则

  • 尊重他人的时间。不要带着问题来,要拿出解决办法。
  • 不要去追逐当下的热点技术,先实现基本功能,然后再做锦上添花的。务实是最重要的。
  • 成为一个“如何做”的团队而不是总是说“不”的团队
  • 预先处理总比亡羊补牢要好,把安全植入到软件开发生命周期中,通过培训开发人员如何写出安全的软件并把它从一开始就作为业务优先考虑之处。

架构

  • 所有 Javascript、CSS 和图像都缓存在 CDN 级别。 DNS 指向一个 CDN,它将请求传递给源服务器。我们使用 Cotendo 是因为它允许在 CDN 上做出 L7 路由决策。
  • 单独的 Web 服务器集群用于为普通用户和广告用户的请求提供服务,通过 cookie 进行区分。
  • 我们正在转向面向服务的架构,其中系统的关键部分,例如搜索、身份验证、缓存,都是以各种语言实现的 RESTFUL 服务。这些服务还提供了一个缓存层。
  • REDIS NOSQL 键值存储(redis.io)用作数据库调用之前的缓存层。
  • Scaleout 用于在网络服务器集群中维护会话状态。但是,我们正在考虑切换到 REDIS。

经验教训

  • SQL Server 数据库中的文本搜索不好用,经常出现 CPU 阻塞,所以 Cinchcast 切换到 ElasticSearch,一个 Lucene 的衍生工具。
  • 微软内置的会话模块容易出现死锁,他们用 AngiesList 会话模块取代了它,并把数据存储到 Redis。
  • 日志是发现问题的关键。
  • 重新发明轮子,有时候也可以是一件好事。例如,在一个供应商的提供的 JS / CSS 的产品导致性能问题的时候,他们通过重写显著改善了网站的性能。
  • 并不是所有的数据都是关系型的。
  • 在开发中不使用指标检测就像在风暴中不参考高度表来降落飞机,因此整个开发过程中,一定要通过网站吞吐量,解决错误的时间、代码覆盖率,等指标来衡量你的效率。 总的来说,对于日 PV 百万级的网站来说,Cinchcast 的架构、研发、运维等层面的技术选型和经验值得学习和参考。

参考资料

亚马逊的架构

摘录的要点

可扩展:添加资源,性能成正比提升

分布式、去中心化

隔离性:面向服务,聚合数以百计的服务,对外统一提供服务

同时支持 REST 和 SOAP

团队在精不在多,节省沟通成本

状态管理是大规模系统的核心问题,如分布式 Session 等

设计应尽量简单,很多问题可以用业务逻辑去解决,而不是通过技术

参考资料

设计 Pastebin.com (或者 Bit.ly)

本文搬运自 设计 Pastebin.com (或者 Bit.ly)

注意: 为了避免重复,当前文档会直接链接到系统设计主题的相关区域,请参考链接内容以获得综合的讨论点、权衡和替代方案。

设计 Bit.ly - 是一个类似的问题,区别是 pastebin 需要存储的是 paste 的内容,而不是原始的未短化的 url。

步骤一、需求分析

收集这个问题的需求和范畴。
问相关问题来明确用例和约束。
讨论一些假设。

用例

问题范围

  • 用户 输入一段文本,然后得到一个随机生成的链接
    • 过期设置
      • 默认的设置是不会过期的
      • 可以选择设置一个过期的时间
  • 用户 输入一个 paste 的 url 后,可以看到它存储的内容
  • 用户 是匿名的
  • Service 跟踪页面分析
    • 一个月的访问统计
  • Service 删除过期的 pastes
  • Service 需要高可用

超出范畴的用例

  • 用户 可以注册一个账户
    • 用户 通过验证邮箱
  • 用户 可以用注册的账户登录
    • 用户 可以编辑文档
  • 用户 可以设置可见性
  • 用户 可以设置短链接

约束和假设

状态假设

  • 访问流量不是均匀分布的
  • 打开一个短链接应该是很快的
  • pastes 只能是文本
  • 页面访问分析数据可以不用实时
  • 一千万的用户量
  • 每个月一千万的 paste 写入量
  • 每个月一亿的 paste 读取量
  • 读写比例在 10:1

性能估算

  • 每个 paste 的大小
    • 每一个 paste 1 KB
    • shortlink - 7 bytes
    • expiration_length_in_minutes - 4 bytes
    • created_at - 5 bytes
    • paste_path - 255 bytes
    • 总共 = ~1.27 KB
  • 每个月新的 paste 内容在 12.7GB
    • (1.27 * 10000000)KB / 月的 paste
    • 三年内将近 450GB 的新 paste 内容
    • 三年内 3.6 亿短链接
    • 假设大部分都是新的 paste,而不是需要更新已存在的 paste
  • 平均 4paste/s 的写入速度
  • 平均 40paste/s 的读取速度

简单的转换指南:

  • 2.5 百万 req/s
  • 1 req/s = 2.5 百万 req/month
  • 40 req/s = 1 亿 req/month
  • 400 req/s = 10 亿 req/month

步骤二、顶层设计

概述一个包括所有重要的组件的高层次设计

Imgur

步骤三、核心组件设计

深入每一个核心组件的细节

用例:用户输入一段文本,然后得到一个随机生成的链接

我们可以用一个 关系型数据库作为一个大的哈希表,用来把生成的 url 映射到一个包含 paste 文件的文件服务器和路径上。

为了避免托管一个文件服务器,我们可以用一个托管的对象存储,比如 Amazon 的 S3 或者NoSQL 文档类型存储

作为一个大的哈希表的关系型数据库的替代方案,我们可以用NoSQL 键值存储。我们需要讨论选择 SQL 或 NoSQL 之间的权衡。下面的讨论是使用关系型数据库方法。

  • 客户端 发送一个创建 paste 的请求到作为一个反向代理启动的 Web 服务器
  • Web 服务器 转发请求给 写接口 服务器
  • 写接口 服务器执行如下操作:
    • 生成一个唯一的 url
      • 检查这个 url 在 SQL 数据库 里面是否是唯一的
      • 如果这个 url 不是唯一的,生成另外一个 url
      • 如果我们支持自定义 url,我们可以使用用户提供的 url(也需要检查是否重复)
    • 把生成的 url 存储到 SQL 数据库pastes 表里面
    • 存储 paste 的内容数据到 对象存储 里面
    • 返回生成的 url

向面试官阐明你需要写多少代码

pastes 表可以有如下结构:

1
2
3
4
5
shortlink char(7) NOT NULL
expiration_length_in_minutes int NOT NULL
created_at datetime NOT NULL
paste_path varchar(255) NOT NULL
PRIMARY KEY(shortlink)

我们将在 shortlink 字段和 created_at 字段上创建一个数据库索引,用来提高查询的速度(避免因为扫描全表导致的长时间查询)并将数据保存在内存中,从内存里面顺序读取 1MB 的数据需要大概 250 微秒,而从 SSD 上读取则需要花费 4 倍的时间,从硬盘上则需要花费 80 倍的时间。 1

为了生成唯一的 url,我们可以:

  • 使用 MD5 来哈希用户的 IP 地址 + 时间戳
    • MD5 是一个普遍用来生成一个 128-bit 长度的哈希值的一种哈希方法
    • MD5 是一致分布的
    • 或者我们也可以用 MD5 哈希一个随机生成的数据
  • Base 62 编码 MD5 哈希值
    • 对于 urls,使用 Base 62 编码 [a-zA-Z0-9] 是比较合适的
    • 对于每一个原始输入只会有一个 hash 结果,Base 62 是确定的(不涉及随机性)
    • Base 64 是另外一个流行的编码方案,但是对于 urls,会因为额外的 +- 字符串而产生一些问题
    • 以下 Base 62 伪代码 执行的时间复杂度是 O(k),k 是数字的数量 = 7:
1
2
3
4
5
6
7
def base_encode(num, base=62):
digits = []
while num > 0
remainder = modulo(num, base)
digits.push(remainder)
num = divide(num, base)
digits = digits.reverse
  • 取输出的前 7 个字符,结果会有 62^7 个可能的值,应该足以满足在 3 年内处理 3.6 亿个短链接的约束:
1
url = base_encode(md5(ip_address+timestamp))[:URL_LENGTH]

我们将会用一个公开的 REST 风格接口

1
$ curl -X POST --data '{"expiration_length_in_minutes":"60", \"paste_contents":"Hello World!"}' https://pastebin.com/api/v1/paste

Response:

1
2
3
{
"shortlink": "foobar"
}

用于内部通信,我们可以用 RPC

用例:用户输入一个 paste 的 url 后可以看到它存储的内容

  • 客户端 发送一个获取 paste 请求到 Web Server
  • Web Server 转发请求给 读取接口 服务器
  • 读取接口 服务器执行如下操作:
    • SQL 数据库 检查这个生成的 url
      • 如果这个 url 在 SQL 数据库 里面,则从 对象存储 获取这个 paste 的内容
      • 否则,返回一个错误页面给用户

REST API:

1
curl https://pastebin.com/api/v1/paste?shortlink=foobar

Response:

1
2
3
4
5
{
"paste_contents": "Hello World",
"created_at": "YYYY-MM-DD HH:MM:SS",
"expiration_length_in_minutes": "60"
}

用例: 服务跟踪分析页面

因为实时分析不是必须的,所以我们可以简单的 MapReduce Web Server 的日志,用来生成点击次数。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
class HitCounts(MRJob):

def extract_url(self, line):
"""Extract the generated url from the log line."""
...

def extract_year_month(self, line):
"""Return the year and month portions of the timestamp."""
...

def mapper(self, _, line):
"""Parse each log line, extract and transform relevant lines.

Emit key value pairs of the form:

(2016-01, url0), 1
(2016-01, url0), 1
(2016-01, url1), 1
"""
url = self.extract_url(line)
period = self.extract_year_month(line)
yield (period, url), 1

def reducer(self, key, values):
"""Sum values for each key.

(2016-01, url0), 2
(2016-01, url1), 1
"""
yield key, sum(values)

用例: 服务删除过期的 pastes

为了删除过期的 pastes,我们可以直接搜索 SQL 数据库 中所有的过期时间比当前时间更早的记录,
所有过期的记录将从这张表里面删除(或者将其标记为过期)。

步骤四、扩展设计

给定约束条件,识别和解决瓶颈。

Imgur

重要提示: 不要简单的从最初的设计直接跳到最终的设计

说明您将迭代地执行这样的操作:1)Benchmark/Load 测试,2)Profile 出瓶颈,3)在评估替代方案和权衡时解决瓶颈,4)重复前面,可以参考在 AWS 上设计一个可以支持百万用户的系统这个用来解决如何迭代地扩展初始设计的例子。

重要的是讨论在初始设计中可能遇到的瓶颈,以及如何解决每个瓶颈。比如,在多个 Web 服务器 上添加 负载平衡器 可以解决哪些问题? CDN 解决哪些问题?Master-Slave Replicas 解决哪些问题? 替代方案是什么和怎么对每一个替代方案进行权衡比较?

我们将介绍一些组件来完成设计,并解决可伸缩性问题。内部的负载平衡器并不能减少杂乱。

为了避免重复的讨论, 参考以下系统设计主题获取主要讨论要点、权衡和替代方案:

分析存储数据库 可以用比如 Amazon Redshift 或者 Google BigQuery 这样的数据仓库解决方案。

一个像 Amazon S3 这样的 对象存储,可以轻松处理每月 12.7 GB 的新内容约束。

要处理 平均 每秒 40 读请求(峰值更高),其中热点内容的流量应该由 内存缓存 处理,而不是数据库。内存缓存 对于处理分布不均匀的流量和流量峰值也很有用。只要副本没有陷入复制写的泥潭,SQL Read Replicas 应该能够处理缓存丢失。

对于单个 SQL Write Master-Slave平均 每秒 4paste 写入 (峰值更高) 应该是可以做到的。否则,我们需要使用额外的 SQL 扩展模式:

我们还应该考虑将一些数据移动到 NoSQL 数据库

额外的话题

是否更深入探讨额外主题,取决于问题的范围和面试剩余的时间。

NoSQL

缓存

异步和微服务

通信

安全

参考安全

延迟数字

每个程序员都应该知道的延迟数

持续进行

  • 继续对系统进行基准测试和监控,以在瓶颈出现时解决它们
  • 扩展是一个迭代的过程

《架构实战案例解析》笔记

架构的本质:如何打造一个有序的系统?

架构的本质:通过合理的内部编排,保证系统高度有序,能够不断扩展,满足业务和技术的变化

  • 首先,架构的出发点是业务和技术在不断复杂化,引起系统混乱,需要通过架构来保证有序。

  • 其次,架构实现从无序到有序,是通过合理的内部编排实现的,基本的手段,就是“分”与“合”,先把系统打散,然后将它们重新组合,形成更合理的关系。

    • “分”就是把系统拆分为各个子系统、模块、组件
    • “合”就是基于业务流程和技术手段,把各个组件有机整合在一起

架构的分类

  • 业务架构
  • 应用架构
  • 技术架构

什么是好架构?

  • 一个好的架构设计既要满足业务的可扩展、可复用;
  • 也要满足系统的高可用、高性能和可伸缩,并尽量采用低成本的方式落地。

架构师的自我修养

  • 优秀的程序员
  • 沟通交流(感性)
  • 权衡取舍(理性)
  • 多领域知识(技术的广度)
  • 技术前瞻性(技术的深度)
  • 看透问题本质(思维的深度)
  • 抽象思维(思维的高度)

业务架构:作为开发,你真的了解业务吗?

从架构角度看,业务架构是源头,然后才是技术架构。

业务架构师和产品经理有什么区别?

  • 产品经理定义了系统的外观
    • 告诉用户,系统长什么样子
    • 告诉开发,要实现什么功能
  • 架构师将业务抽象为结构化的模块体系
    • 把业务流程拆分,按照业务域的维度来划分系统模块。
    • 并定义这些模块之间的关系,最终形成一个高度结构化的模块体系。

架构目标之业务的可扩展

业务的主题是变化和创新,系统的主题是稳定和可靠

架构目标之业务的可复用

业务架构设计如何实现业务的可复用呢

首先,模块的职责定位要非常清晰。对于模块来说,在定位范围内的职责要全部涵盖到,而不在这个范围的职责全部不要。

其次,模块的数据模型和接口设计要保证通用。架构师需要归纳业务场景,通过抽象提炼,形成通用化的设计,以此来满足多个类似场景的需求。

最后,实现模块的高复用,还需要做好业务的层次划分。我们知道,越是底层的业务,它就相对更固定。举个例子,同样是订单业务域,对于底层订单的增删改查功能,不同类型的订单都是一样的,但对于上层的订单生命周期管理,外卖订单和堂食订单可能就不一样。

可扩展架构:如何打造一个善变的柔性系统?

系统的构成:模块 + 关系

模块是系统的基本组成部分,它泛指子系统、应用、服务或功能模块。关系指模块之间的依赖关系。

模块的要求:

  • 定位明确,概念完整
  • 自成体系,粒度适中

依赖关系的要求:

  • 最好是单向的
  • 最好是层次化结构

模块的业务逻辑尽量围绕自身内部数据进行处理,对外部依赖越小,模块的封装性越好,稳定性也越强,不会随着外部模块的调整而调整。

业务架构扩展性的本质是:通过构建合理的模块体系,有效地控制系统复杂度,最小化业务变化引起的系统调整。

那如何打造一个合理的模块体系呢?具体的架构手段就是按照业务对系统进行拆分和整合:
通过拆分,实现模块划分;通过整合,优化模块依赖关系。

通过模块通用化,模块的数量减少了,模块的定位更清晰,概念更完整,职责更聚焦。在实
践中,当不同业务线对某个功能需求比较类似时,我们经常会使用这个手段。

通过拆分,实现模块划分;通过整合,优化模块依赖关系。

一般做业务架构时,我们先考虑垂直拆分,从大方向上,把不同业务给区分清楚,然后再针对具体业务,按照业务处理流程进行水平拆分

业务平台化是模块依赖关系层次化的一个特例,只是它偏向于基础能力,在实践中,当业务
线很多,业务规则很复杂时,我们经常把底层业务能力抽取出来,进行平台化处理。

可扩展架构案例(一):电商平台架构是如何演变的?

电商平台架构发展的大致过程:

单体架构

在单体架构中,只有一个应用,所有代码跑在一个进程,所有的表放在一个 DB 里。

单体应用内部一般采用分层结构,从上到下,一般分为表示层、业务层、数据访问层、DB 层。表示层负责用户体验,业务层负责业务逻辑,数据访问层负责 DB 的数据存取。

分布式架构

分布式架构,简单来说就是系统由多个独立的应用组成,它们互相协作,成为一个整体。

分布式架构包括了多个应用,每个应用分别负责不同的业务线,当一个应用需要另一个应用的功能时,会通过 API 接口进行调用。在分布式架构中,API 接口属于应用的一部分,它和表示层共享底层的业务逻辑。

分布式架构适用于业务相关性低、耦合少的业务系统。

SOA 架构

SOA 架构(Service Oriented Architecture)是一种面向服务的架构,它的发展经历了两个阶段:传统的 SOA 架构,它解决的是企业内部大量异构系统集成的问题;新的 SOA 架构,它解决的是系统重复建设的问题。

在 SOA 架构中,每个服务都对应一个现有的系统,所有这些服务都部署在一个中心化的平台上,我们称之为企业服务总线 ESB(Enterprise Service Bus),ESB 负责管理所有调用过程的技术复杂性,包括服务的注册和路由、各种通信协议的支持等等。

微服务架构

微服务强调围绕业务,进行清晰的业务和数据边界划分,并通过良好定义的接口输出业务能力,这和 SOA 架构里的服务有点类似。但两者不同的地方在于,微服务是去中心化的,不需要 SOA 架构中 ESB 的集中管理方式。

一方面,微服务强调所谓的哑管道,即客户端可以通过 HTTP 等简单的技术手段,访问微服务,避免重的通信协议和数据编码支持。另一方面,微服务强调智能终端,所有的业务逻辑包含在微服务内部,不需要额外的中间层提供业务规则处理。

可扩展架构案例(二):App 服务端架构是如何升级的?

V1.0 架构

问题:

  • 移动服务端对 Jar 包的紧密依赖
  • 移动团队的职责过分复杂
  • 团队并行开发困难

V2.0 架构

问题:

  • 移动端和 PC 端互相干扰
  • 重复造轮子
  • 稳定性较差

V3.0 架构

首先,我们对每个业务线的服务端进行拆分,让 App 接口和 PC 端接口各自在物理上独立,但它们共享核心的业务逻辑。

移动网关的内部实现

  • 通用层
    • 首先是通用层,它负责所有系统级功能的处理,比如通讯协议适配、安全、监控、日志等等,这些功能统一由网关的通用层进行预处理,避免了各个业务线的重复开发。
    • 在具体实现时,每个通用功能的处理逻辑都会封装成一个拦截器,这些拦截器遵循统一的接口定义,并且拦截器都是可配置的。当有外部请求过来,网关会依次调用这些拦截器,完成各个系统级功能的处理。
  • 接口路由层
    • 移动端请求经过通用层的预处理之后,将会进一步分发给后端的业务适配器进行处理。
    • 在配置文件里,对接口请求的 URL 和业务适配器进行映射,接口路由层的分发逻辑就是根据请求中的 URL,在配置文件里找到对应的适配器,然后把请求交给适配器进行后续的处理。
  • 服务适配层
    • 适配器首先用来解决内外部接口的适配,除此之外,适配器还可以根据需要,对多个内部服
      务做业务聚合,这样可以对 App 前端提供粗粒度的接口服务,减少远程网络的调用次数。

可扩展架构案例(三):你真的需要一个中台吗?

前台:面向 C 端的应用。前台对外

后台:企业内部系统。后台对内

中台:通过实现基础业务的平台化,实现了企业级业务能力的快速复用

中台的适用性

第一种是独立地建设新业务线,这样,各个业务线并列,系统整体上是一个“川”字型的结构

第二种做法是,把各业务线中相同的核心逻辑抽取出来,通过抽象设计,实现通用化,共同服务于所有业务线的需求,系统结构整体上是一个“山”字型。这样,我们就能一处建设,多处复用,一处修改,多处变化,从而实现最大程度的复用。

何时从“川”字型转为“山”字形呢?

  • 一方面,这和公司业务线的数量有关,业务线越多,意味着重复建设的成本会更大,当我们开始上第 3 条业务线时,就应该要考虑转到“山”字形了。
  • 另一方面,也和各个业务线的相似度有关,相似度越高,意味着业务线之间有更多类似的逻辑,更适合“山”字形。

中台实现了通用基础业务的平台化。从变化速度来看,企业基础的业务是相对固定的,而具体上层业务场景是相对多变的;从数量来看,基础业务数量是有限的,而具体业务场景是无限的。因此,有了完善的中台,我们就可以通过有限而比较固定的基础业务,来满足无限而快速变化的上层业务场景了。

从业务角度来看,中台收敛了业务场景,统一了业务规则;从系统角度看,中台相当于操作系统,对外提供标准接口,屏蔽了底层系统的复杂性;从数据角度看,中台收敛了数据,比如使用同一套订单数据模型,让所有渠道的订单使用相同的订单模型,所有订单数据落到同一个订单库。

中台通过实现基础业务的平台化,实现了企业级业务能力的快速复用。

松散的微服务 -> 共享服务体系 -> 中台

传统企业中台架构设计

中台代表了企业核心的业务能力,它自成体系,能够为 C 端的互联网场景提供通用的能力,并通过各种插件和后台打通。

对于互联网企业来说,有大量微服务做基础,往中台转是改良,目的是更好地衔接前台和后台,实现业务的快速创新;
对于传统企业来说,内部有大量的遗留系统,落地中台是革命,目的是盘活老系统,全面实现企业的数字化转型。

可复用架构:如何实现高层次的复用?

从复用的程度来看,从高到低,我们可以依次划分为产品复用 > 业务流程复用 > 业务实体复用 > 组件复用 > 代码复用。

技术复用:代码级复用和技术组件复用都属于工具层面,它们的好处是在很多地方都可以用,但和业务场景隔得有点远,不直接对应业务功能,因此复用的价值相对比较低。

业务复用

  • 业务实体复用针对细分的业务领域
  • 业务流程的复用针对的是业务场景
  • 最高层次的复用是对整个系统的复用

可复用架构案例(一):如何设计一个基础服务?

对于落地一个共享服务来说,服务边界的划分和功能的抽象设计是核心。

可复用架构案例(二):如何对现有系统做微服务改造?

圈表:圈表就是用来确定库存微服务具体包含哪些表,也就是确定服务的数据模型。

收集 SQL:收集所有业务系统访问这些表的 SQL 语句,包括它的业务场景说明、访问频率等等。库存微服务后续就针对这些 SQL 进行封装,提供相应的接口给业务系统使用。

拆分 SQL:有些 SQL 不仅仅访问圈定的这几张库存表,还会和产品库中的其他表进行关联。

可复用架构案例(三):中台是如何炼成的?

  1. 业务上有什么重大变化,导致当前系统的弊端已经很明显,不能适应业务发展了呢?
  2. 架构改造时,如何在业务、系统、资源三者之间做好平衡,对系统进行分步式的改造呢?

技术架构:作为开发,你真的了解系统吗?

技术架构的职责,首先是负责系统所有组件的技术选型,然后确保这些组件可以正常运行。

业务架构解决的是系统功能性问题

技术架构解决的是系统非功能性问题

技术架构目标

  • 高可用
  • 高性能
  • 伸缩性
  • 安全性

高可用架构:如何让你的系统不掉链子?

故障分类

  • 资源不可用,包括网络和服务器出故障,网络出故障表明节点连接不上,服务器出故障表明该节点本身不能正常工作。
  • 资源不足,常规的流量进来,节点能正常工作,但在高并发的情况下,节点无法正常工作,对外表现为响应超时。
  • 节点的功能有问题,这个主要体现在我们开发的代码上,比如它的内部业务逻辑有问题,或者是接口不兼容导致客户端调用出了问题;另外有些不够成熟的中间件,有时也会有功能性问题。

高可用策略和架构原则

事前,尽量避免问题的发生;始终,要考虑转移故障,降低故障影响,快速恢复系统;事后,要对故障进行复盘,考虑技术、流程上的完善措施。

高可用架构案例(一):如何实现 O2O 平台日订单 500 万?

高可用架构案例(二):如何第一时间知道系统哪里有问题?

高可用架构案例(三):如何打造一体化的监控系统?

高性能和可伸缩架构:业务增长,能不能加台机器就搞定?

  • 加快单个请求处理
    • 优化处理路径上每个节点的处理速度
    • 并行处理单个请求
  • 同时处理多个请求:负载均衡
  • 请求处理异步化:MQ

性能提升思路:

  • 可水平拆分和无状态
  • 短事务和柔性事务
  • 缓存
  • 并行计算
  • 异步处理
  • 容器化

高性能架构案例:如何设计一个秒杀系统?

可伸缩架构案例:数据太多,如何无限扩展你的数据库?

案例:电商平台技术架构是如何演变的?

单体架构

SOA 架构

微服务架构

垂直拆分(分库)

水平拆分

多机房部署

服务调用本地化

依赖分级管理

多机房独立部署

从务实的角度,给你架构设计的重点知识和学习路径

参考资料

架构实战案例解析

《数据密集型应用系统设计》笔记一之分布式数据系统

出于以下目的,我们需要在多台机器上分布数据:

  • 扩展性:当数据量或者读写负载巨大,严重超出了单台机器的处理上限,需要将负载分散到多台机器上。
  • 容错与高可用性:当单台机器(或者多台,以及网络甚至整个数据中心)出现故障,还希望应用系统可以继续工作,这时需要采用多台机器提供冗余。这样某些组件失效之后,冗余组件可以迅速接管。
  • 延迟考虑:如果客户遍布世界各地,通常需要考虑在全球范围内部署服务,以方便用户就近访问最近数据中心所提供的服务,从而避免数据请求跨越了半个地球才能到达目标。

将数据分布在多节点时有两种常见的方式:

  • 复制:在多个节点上保存相同数据的副本,每个副本具体的存储位置可能不尽相同。复制方住可以提供冗余
  • 分区:将一个大块头的数据库拆分成多个较小的子集即分区,不同的分区分配给不同的节点(也称为分片)。我们在第 6 章主要介绍分区技术。

单主节点数据复制

复制主要指通过网络在多台机器上保存相同数据的副本。

数据复制的作用:

  • 使数据在地理位置上更接近用户,从而降低访问延迟
  • 当部分组件出现位障,系统依然可以继续工作,从而提高可用性
  • 扩展至多台机器以同时提供数据访问服务,从而提高读吞吐量

复制方式:

  • 主从复制
  • 多主节点复制
  • 无主节点复制

复制需要考虑的问题:

  • 同步还是异步
  • 如何处理失败的副本
  • 如何保证数据一致

主节点与从节点

每个保存数据库完整数据集的节点称之为副本。有了多副本,必然会面临一个问题:如何确保所有副本之间的数据是一致的?

主从复制的工作原理如下:

  1. 指定某一个副本为主副本(或称为主节点) 。当客户写数据库时,必须将写请求首先发送给主副本,主副本首先将新数据写入本地存储。
  2. 其他副本则全部称为从副本(或称为从节点)。主副本把新数据写入本地存储后,然后将数据更改作为复制的日志或更改流发送给所有从副本。每个从副本获得更改日志之后将其应用到本地,且严格保持与主副本相同的写入顺序。
  3. 客户端从数据库中读数据时,可以在主副本或者从副本上执行查询。再次强调,只有主副本才可以接受写请求:从客户端的角度来看,从副本都是只读的。

img

典型应用:

  • 数据库:MySql、MongoDB 等
  • 消息队列:Kafka、RabbitMQ 等

同步复制与异步复制

基本流程是,客户将更新请求发送给主节点,主节点接收到请求,接下来将数据更新转发给从节点。最后,由
主节点来通知客户更新完成。

img

通常情况下, 复制速度会非常快,例如多数数据库系统可以在一秒之内完成所有从节点的更新。但是,系统其
实并没有保证一定会在多段时间内完成复制。有些情况下,从节点可能落后主节点几分钟甚至更长时间,例如,由于从节点刚从故障中恢复,或者系统已经接近最大设计上限,或者节点之间的网络出现问题。

  • 同步复制的优点: 一旦向用户确认,从节点可以明确保证完成了与主节点的更新同步,数据已经处于最新版本。万一主节点发生故障,总是可以在从节点继续访问最新数据。
  • 同步复制的缺点:如果同步的从节点无法完成确认(例如由于从节点发生崩愤,或者网络故障,或任何其他原因), 写入就不能视为成功。主节点会阻塞其后所有的写操作,直到同步副本确认完成。

因此,把所有从节点都配置为同步复制有些不切实际。因为这样的话,任何一个同步节点的中断都会导致整个系统更新停滞不前。

实际应用中,很多数据库推荐的模式是:只要有一个从节点或半数以上的从节点同步成功,就视为同步,直接返回结果;剩下的节点都通过异步方式同步。

  • 异步复制的优点:不管从节点上数据多么滞后,主节点总是可以继续响应写请求,系统的吞吐性能更好。
  • 异步复制的缺点:如果主节点发生失败且不可恢复,则所有尚未复制到从节点的写请求都会丢失。

主从复制还经常会被配置为全异步模式。此时如果主节点发生失败且不可恢复,则所有尚未复制到从节点的写请求都会丢失。这意味着即使向客户端确认了写操作, 却无法保证数据的持久化。

配置新的从节点

配置新的从节点的主要操作步骤:

  1. 在某个时间点对主节点的数据副本产生一个一致性快照,这样避免长时间锁定整个数据库。目前大多数数据库都支持此功能,快照也是系统备份所必需的。而在某些情况下,可能需要第三方工具, 如 MySQL 的 innobackupex。
  2. 将此快照拷贝到新的从节点。
  3. 从节点连接到主节点并请求快照点之后所发生的数据更改日志。因为在第一步创建快照时,快照与系统复制日志的某个确定位置相关联,这个位置信息在不同的系统有不同的称呼,如 PostgreSQL 将其称为“ log sequence number” (日志序列号),而 MySQL 将其称为“ binlog coordinates ” 。
  4. 获得日志之后,从节点来应用这些快照点之后所有数据变更,这个过程称之为追赶。接下来,它可以继续处理主节点上新的数据变化。井重复步骤 1 ~步骤 4 。

建立新的从副本具体操作步骤可能因数据库系统而异。

处理节点失效

如何通过主从复制技术来实现系统高可用呢?

从节点失效: 追赶式恢复

从节点的本地磁盘上都保存了副本收到的数据变更日志。如果从节点发生崩溃,然后顺利重启,或者主从节点之间的网络发生暂时中断(闪断),则恢复比较容易,根据副本的复制日志,从节点可以知道在发生故障之前所处理的最后一笔事务,然后连接到主节点,并请求自那笔事务之后中断期间内所有的数据变更。在收到这些数据变更日志之后,将其应用到本地来追赶主节点。之后就和正常情况一样持续接收来自主节点数据流的变化。

主节点失效:节点切换

选择某个从节点将其提升为主节点;客户端也需要更新,这样之后的写请求会发送给新的主节点,然后其他从节点要接受来自新的主节点上的变更数据,这一过程称之为切换。

步骤通常如下:

  1. 确认主节点失效。有很多种出错可能性,所以大多数系统都采用了基于超时的机制:节点间频繁地互相发生发送心跳悄息,如果发现某一个节点在一段比较长时间内(例如 30s )没有响应,即认为该节点发生失效。
  2. 选举新的主节点。可以通过选举的方式(超过多数的节点达成共识)来选举新的主节点,或者由之前选定的某控制节点来指定新的主节点。候选节点最好与原主节点的数据差异最小,这样可以最小化数据丢失的风险。让所有节点同意新的主节点是个典型的共识问题。
  3. 重新配置系统使新主节点生效。客户端现在需要将写请求发送给新的主节点。如果原主节点之后重新上线,可能仍然自认为是主节点,而没有意识到其他节点已经达成共识迫使其下台。这时系统要确保原主节点降级为从节点,并认可新的主节点。

上述切换过程依然充满了很多变数:

  • 如果使用了异步复制,且失效之前,新的主节点并未收到原主节点的所有数据;在选举之后,原主节点很快又重新上线并加入到集群,接下来的写操作会发生什么?新的主节点很可能会收到冲突的写请求,这是因为原主节点未意识的角色变化,还会尝试同步其他从节点,但其中的一个现在已经接管成为现任主节点。常见的解决方案是,原主节点上未完成复制的写请求就此丢弃,但这可能会违背数据更新持久化的承诺。
  • 如果在数据库之外有其他系统依赖于数据库的内容并在一起协同使用,丢弃数据的方案就特别危险。例如,在 GitHub 的一个事故中,某个数据并非完全同步的 MySQL 从节点被提升为主副本,数据库使用了自增计数器将主键分配给新创建的行,但是因为新的主节点计数器落后于原主节点( 即二者并非完全同步),它重新使用了已被原主节点分配出去的某些主键,而恰好这些主键已被外部 Redis 所引用,结果出现 MySQL 和 Redis 之间的不一致,最后导致了某些私有数据被错误地泄露给了其他用户。
  • 在某些故障情况下,可能会发生两个节点同时-都自认为是主节点。这种情况被称为脑裂,它非常危险:两个主节点都可能接受写请求,并且没有很好解决冲突的办法,最后数据可能会丢失或者破坏。作为一种安全应急方案,有些系统会采取措施来强制关闭其中一个节点。然而,如果设计或者实现考虑不周,可能会出现两个节点都被关闭的情况。
  • 如何设置合适的超时来检测主节点失效呢? 主节点失效后,超时时间设置得越长也意味着总体恢复时间就越长。但如果超时设置太短,可能会导致很多不必要的切换。例如,突发的负载峰值会导致节点的响应时间变长甚至超肘,或者由于网络故障导致延迟增加。如果系统此时已经处于高负载压力或网络已经出现严重拥塞,不必要的切换操作只会使总体情况变得更糟。

复制日志的实现

基于语句的复制

最简单的情况,主节点记录所执行的每个写请求(操作语句)井将该操作语句作为日志发送给从节点。对于关系数据库,这意味着每个 INSERT 、UPDATE 或 DELETE 语句都会转发给从节点,并且每个从节点都会分析井执行这些 SQU 吾句,如同它们是来自客户端那样。

听起来很合理也不复杂,但这种复制方式有一些不适用的场景:

  • 任何调用非确定性函数的语句,如 NOW() 获取当前时间,或 RAND() 获取一个随机数等,可能会在不同的副本上产生不同的值。
  • 如果语句中使用了自增列,或者依赖于数据库的现有数据(例如, UPDATE ... WHERE <某些条件>),则所有副本必须按照完全相同的顺序执行,否则可能会带来不同的结果。进而,如果有多个同时并发执行的事务时, 会有很大的限制。
  • 有副作用的语句(例如,触发器、存储过程、用户定义的函数等),可能会在每个副本上产生不同的副作用。

有可能采取一些特殊措施来解决这些问题,例如,主节点可以在记录操作语句时将非确定性函数替换为执行之后的确定的结果,这样所有节点直接使用相同的结果值。但是,这里面存在太多边界条件需要考虑,因此目前通常首选的是其他复制实现方案。

MySQL 5.1 版本之前采用基于操作语句的复制。现在由于逻辑紧凑,依然在用,但是默认情况下,如果语句中存在一些不确定性操作,则 MySQL 会切换到基于行的复制(稍后讨论)。VoltDB 使用基于语句的复制,它通过事务级别的确定性来保证复制的安全。

基于预写日志(WAL)传输

通常每个写操作都是以追加写的方式写入到日志中:

  • 对于日志结构存储引擎,日志是主要的存储方式。日志段在后台压缩井支持垃圾回收。
  • 对于采用覆写磁盘的 BTree 结构,每次修改会预先写入日志,如系统发生崩溃,通过索引更新的方式迅速恢复到此前一致状态。

不管哪种情况,所有对数据库写入的字节序列都被记入日志。因此可以使用完全相同的日志在另一个节点上构建副本:除了将日志写入磁盘之外, 主节点还可以通过网络将其发送给从节点。

PostgreSQL 、Oracle 以及其他系统等支持这种复制方式。其主要缺点是日志描述的数据结果非常底层: 一个 WAL 包含了哪些磁盘块的哪些字节发生改变,诸如此类的细节。这使得复制方案和存储引擎紧密搞合。如果数据库的存储格式从一个版本改为另一个版本,那么系统通常无能支持主从节点上运行不同版本的软件。

基于行的逻辑日志复制

关系数据库的逻辑日志通常是指一系列记录来描述数据表行级别的写请求:

  • 对于行插入,日志包含所有相关列的新值。
  • 对于行删除,日志里有足够的信息来唯一标识已删除的行,通常是靠主键,但如果表上没有定义主键,就需要记录所有列的旧值。
  • 对于行更新,日志包含足够的信息来唯一标识更新的行,以及所有列的新值(或至少包含所有已更新列的新值)。

如果一条事务涉及多行的修改,则会产生多个这样的日志记录,并在后面跟着一条记录,指出该事务已经提交。MySQL 的二进制日志 binlog (当配置为基于行的复制时)使用该方式。

由于逻辑日志与存储引擎逻辑解耦,因此可以更容易地保持向后兼容,从而使主从节点能够运行不同版本的软件甚至是不同的存储引擎。

对于外部应用程序来说,逻辑日志格式也更容易解析。

基于触发器的复制

在某些情况下,我们可能需要更高的灵活性。例如,只想复制数据的一部分,或者想从一种数据库复制到另一种数据库,或者需要订制、管理冲突解决逻辑( 参阅本章后面的“处理写冲突”),则需要将复制控制交给应用程序层。

有一些工具,可以通过读取数据库日志让应用程序获取数据变更。另一种方法则是借助许多关系数据库都支持的功能:触发器和存储过程。

触发器支持注册自己的应用层代码,使得当数据库系统发生数据更改(写事务)时自动执行上述自定义代码。通过触发器技术,可以将数据更改记录到一个单独的表中,然后外部处理逻辑访问该表,实施必要的自定义应用层逻辑,例如将数据更改复制到另一个系统。Oracle 的 Databus 和 Postgres 的 Bucardo 就是这种技术的典型代表。基于触发器的复制通常比其他复制方式开销更高, 也比数据库内置复制更容易出错,或者暴露一些限制。然而,其高度灵活性仍有用武之地。

复制滞后问题

主从复制要求所有写请求都经由主节点,而任何副本只能接受只读查询。对于读操作密集的负载(如 Web ),这是一个不错的选择:创建多个从副本,将读请求分发给这些从副本,从而减轻主节点负载井允许读取请求就近满足。

在这种扩展体系下,只需添加更多的从副本,就可以提高读请求的服务吞吐量。但是,这种方法实际上只能用于异步复制,如果试图同步复制所有的从副本,则单个节点故障或网络中断将使整个系统无法写入。而且节点越多,发生故障的概率越高,所以完全同步的配置现实中反而非常不可靠。

不幸的是,如果一个应用正好从一个异步的从节点读取数据,而该副本落后于主节点,则应用可能会读到过期的信息。这会导致数据库中出现明显的不一致:由于并非所有的写入都反映在从副本上,如果同时对主节点和从节点发起相同的查询,可能会得到不同的结果。经过一段时间之后,从节点最终会赶上并与主节点数据保持一致。这种效应也被称为最终一致性。

读自己的写

许多应用让用户提交一些数据,接下来查看他们自己所提交的内容。例如客户数据库中的记录,亦或者是讨论主题的评论等。提交新数据须发送到主节点,但是当用户读取数据时,数据可能来自从节点。这对于读密集和偶尔写入的负载是个非常合适的方案。

然而对于异步复制存在这样一个问题,如图 5-3 所示,用户在写人不久即查看数据,则新数据可能尚未到达从节点。对用户来讲, 看起来似乎是刚刚提交的数据丢失了,显然用户不会高兴。

img

对于这种情况,我们需要强一致性。如何实现呢?有以下方案:

  • 如果用户访问可能会被修改的内容,从主节点读取; 否则,在从节点读取。这背后就要求有一些方法在实际执行查询之前,就已经知道内容是否可能会被修改。例如,社交网络上的用户首页信息通常只能由所有者编辑,而其他人无法编辑。因此,这就形成一个简单的规则: 总是从主节点读取用户自己的首页配置文件,而在从节点读取其他用户的配置文件。
  • 如果应用的大部分内容都可能被所有用户修改,那么上述方法将不太有效,它会导致大部分内容都必须经由主节点,这就丧失了读操作的扩展性。此时需要其他方案来判断是否从主节点读取。例如,跟踪最近更新的时间,如果更新后一分钟之内,则总是在主节点读取;井监控从节点的复制滞后程度,避免从那些滞后时间超过一分钟的从节点读取。
  • 客户端还可以记住最近更新时的时间戳,井附带在读请求中,据此信息,系统可以确保对该用户提供读服务时都应该至少包含了该时间戳的更新。如果不够新,要么交由另一个副本来处理,要么等待直到副本接收到了最近的更新。时间戳可以是逻辑时间戳(例如用来指示写入顺序的日志序列号)或实际系统时钟(在这
    种情况下,时钟同步又称为一个关键点, 请参阅第 8 章“不可靠的时钟”)。
  • 如果副本分布在多数据中心(例如考虑与用户的地理接近,以及高可用性),情况会更复杂些。必须先把请求路由到主节点所在的数据中心(该数据中心可能离用户很远)。

如果同一用户可能会从多个设备访问数据,情况会更加复杂。

  • 记住用户上次更新时间戳的方法实现起来会比较困难,因为在一台设备上运行的代码完全无法知道在其他设备上发生了什么。此时,元数据必须做到全局共享。
  • 如果副本分布在多数据中心,无法保证来自不同设备的连接经过路由之后都到达同一个数据中心。例如,用户的台式计算机使用了家庭宽带连接,而移动设备则使用蜂窝数据网络,不同设备的网络连接线路可能完全不同。如果方案要求必须从主节点读取,则首先需要想办毡确保将来自不同设备的请求路由到同一个数据中心。

单调读

img

用户看到了最新内窑之后又读到了过期的内容,好像时间被回拨, 此时需要单调读一致性。

单调读一致性可以确保不会发生这种异常。这是一个比强一致性弱,但比最终一致性强的保证。当读取数据时,单调读保证,如果某个用户依次进行多次读取,则他绝不会看到回攘现象,即在读取较新值之后又发生读旧值的情况。

实现单调读的一种方式是,确保每个用户总是从固定的同一副本执行读取(而不同的用户可以从不同的副本读取)。

前缀一致读

前缀一致读:对于一系列按照某个顺序发生的写请求,那么读取这些内容时也会按照当时写入的顺序。

如果数据库总是以相同的顺序写入,则读取总是看到一致的序列,不会发生这种反常。然而,在许多分布式数据库中,不同的分区独立运行,因此不存在全局写入顺序。这就导致当用户从数据库中读数据时,可能会看到数据库的某部分旧值和另一部分新值。

多主节点复制

主从复制存在一个明显的缺点:系统只有一个主节点,而所有写人都必须经由主节点。主从复制方案就会影响所有的写入操作。

适用场景

在一个数据中心内部使用多主节点基本没有太大意义,其复杂性已经超过所能带来的好处。

以下场景这种配置则是合理的:

  • 多数据中心
  • 离线客户端操作
  • 协作编辑

多数据中心

部署单主节点的主从复制方案与多主复制方案之间的差异

  • 性能:对于主从复制,每个写请求都必须经由广域网传送至主节点所在的数据中心。这会大大增加写入延迟,井基本偏离了采用多数据中心的初衷(即就近访问)。而在多主节点模型中,每个写操作都可以在本地数据中心快速响应,然后采用异步复制方式将变化同步到其他数据中心。因此,对上层应用有效屏蔽了数据中心之间的网络延迟,使得终端用户所体验到的性能更好。
  • 容忍数据中心失效:对于主从复制,如果主节点所在的数据中心发生故障,必须切换至另一个数据中心,将其中的一个从节点被提升为主节点。在多主节点模型中,每个数据中心则可以独立于其他数据中心继续运行,发生故障的数据中心在恢复之后更新到最新状态。
  • 容忍网络问题:数据中心之间的通信通常经由广域网,它往往不如数据中心内的本地网络可靠。对于主从复制模型,由于写请求是同步操作,对数据中心之间的网络性能和稳定性等更加依赖。多主节点模型则通常采用异步复制,可以更好地容忍此类问题,例如临时网络闪断不会妨碍写请求最终成功。

处理写冲突

多主复制的最大问题是可能发生写冲突。

同步与异步冲突检测

理论上, 也可以做到同步冲突检测,即等待写请求完成对所有副本的同步,然后再通知用户写入成功。但是,这样做将会失去多主节点的主要优势:允许每个主节点独立接受写请求。如果确实想要同步方式冲突检测,或许应该考虑采用单主节点的主从复制模型。

避免冲突

处理冲突最理想的策略是避免发生冲突,即如果应用层可以保证对特定记录的写请求总是通过同一个主节点,这样就不会发生写冲突。现实中,由于不少多主节点复制模型所实现的冲突解决方案存在瑕疵,因此,避免冲突反而成为大家普遍推荐的首选方案。

但是,有时可能需要改变事先指定的主节点,例如由于该数据中心发生故障,不得不将流量重新路由到其他数据中心,或者是因为用户已经漫游到另一个位置,因而更靠近新数据中心。此时,冲突避免方式不再有效,必须有措施来处理同时写入冲突的可能性。

收敛于一致状态

对干主从复制模型,数据更新符合顺序性原则,即如果同一个字段有多个更新,则最后一个写操作将决定该字段的最终值。

对于多主节点复制模型,由于不存在这样的写入顺序,所以最终值也会变得不确定。

实现收敛的冲突解决有以下可能的方式:

  • 给每个写入分配唯一的 ID ,例如, 一个时间戳, 二个足够长的随机数, 一个 UUID 或者一个基于键-值的 Jl 合希,挑选最高 ID 的写入作为胜利者,并将其他写入丢弃。如果基于时间戳,这种技术被称为最后写入者获胜。虽然这种方陆很流行,但是很容易造成数据丢失。
  • 为每个副本分配一个唯一的 ID ,井制定规则,例如序号高的副本写入始终优先于序号低的副本。这种方法也可能会导致数据丢失。
  • 以某种方式将这些值合并在一起。例如,按字母顺序排序,然后拼接在一起
  • 利用预定义好的格式来记录和保留冲突相关的所有信息,然后依靠应用层的逻辑,事后解决冲突(可能会提示用户) 。

自定义冲突解决逻辑

解决冲突最合适的方式可能还是依靠应用层,所以大多数多主节点复制模型都有工具来让用户编写应用代码来解决冲突。可以在写入时或在读取时执行这些代码逻辑:

  • 在写入时执行:只要数据库系统在复制变更日志时检测到冲突,就会调用应用层的冲突处理程序。
  • 在读取时执行:当检测到冲突时,所有冲突写入值都会暂时保存下来。下一次读取数据时,会将数据的多个版本读返回给应用层。应用层可能会提示用户或自动解决冲突, 井将最后的结果返回到数据库。

无主节点复制

客户端将写请求发送到多个节点上,读取时从多个节点上并行读取,以此检测和纠正某些过期数据。

数据分区

在不同系统中,分区有着不同的称呼,例如它对应于 MongoDB, Elasticsearch 和 SolrCloud 中的 shard, HBase 的 region, Bigtable 中的
tablet, Cassandra 和 Riak 中的 vnode ,以及 Couch base 中的 vBucket。总体而言,分区是最普遍的术语。

采用数据分区的主要目的是提高可扩展性。不同的分区可以放在不同的节点上,这样一个大数据集可以分散在更多的磁盘上,查询负载也随之分布到更多的处理器上。

对单个分区进行查询时,每个节点对自己所在分区可以独立执行查询操作,因此添加更多的节点可以提高查询吞吐量。超大而复杂的查询尽管比较困难,但也可能做到跨节点的并行处理。

数据分区与数据复制

分区通常与复制结合使用,即每个分区在多个节点都存有副本。这意味着某条记录属于特定的分区,而同样的内容会保存在不同的节点上以提高系统的容错性。

一个节点上可能存储了多个分区。每个分区都有自己的主副本,例如被分配给某节点,而从副本则分配在其他一些节点。一个节点可能即是某些分区的主副本,同时又是其他分区的从副本。

键-值数据的分区

分区的主要目标是将数据和查询负载均匀分布在所有节点上。如果节点平均分担负载,那么理论上 10 个节点应该能够处理 10 倍的数据量和 10 倍于单个节点的读写吞吐量(忽略复制) 。

而如果分区不均匀,则会出现某些分区节点比其他分区承担更多的数据量或查询负载,称之为倾斜。倾斜会导致分区效率严重下降,在极端情况下,所有的负载可能会集中在一个分区节点上,这就意味着 10 个节点 9 个空闲,系统的瓶颈在最繁忙的那个节点上。这种负载严重不成比例的分区即成为系统热点。

基于关键字区间分区

一种分区方式是为每个分区分配一段连续的关键字或者关键宇区间范围(以最小值和最大值来指示)。

关键字的区间段不一定非要均匀分布,这主要是因为数据本身可能就不均匀。

每个分区内可以按照关键字排序保存(参阅第 3 章的“ SSTables 和 LSM Trees ”)。这样可以轻松支持区间查询,即将关键字作为一个拼接起来的索引项从而一次查询得到多个相关记录。

然而,基于关键字的区间分区的缺点是某些访问模式会导致热点。如果关键字是时间戳,则分区对应于一个时间范围,所有的写入操作都集中在同一个分区(即当天的分区),这会导致该分区在写入时负载过高,而其他分区始终处于空闲状态。为了避免上述问题,需要使用时间戳以外的其他内容作为关键字的第一项。

基于关键字晗希值分区

一且找到合适的关键宇 H 合希函数,就可以为每个分区分配一个哈希范围(而不是直接作用于关键宇范围),关键字根据其哈希值的范围划分到不同的分区中。

img

这种方总可以很好地将关键字均匀地分配到多个分区中。分区边界可以是均匀间隔,也可以是伪随机选择( 在这种情况下,该技术有时被称为一致性哈希) 。

然而,通过关键宇 II 合希进行分区,我们丧失了良好的区间查询特性。即使关键字相邻,但经过哈希之后会分散在不同的分区中,区间查询就失去了原有的有序相邻的特性。

负载倾斜与热点

基于哈希的分区方能可以减轻热点,但无住做到完全避免。一个极端情况是,所有的读/写操作都是针对同一个关键字,则最终所有请求都将被路由到同一个分区。

一个简单的技术就是在关键字的开头或结尾处添加一个随机数。只需一个两位数的十进制随机数就可以将关键字的写操作分布到 100 个不同的关键字上,从而分配到不同的分区上。但是,随之而来的问题是,之后的任何读取都需要些额外的工作,必须从所有 100 个关键字中读取数据然后进行合井。因此通常只对少量的热点关键字附加随机数才有意义。

分区与二级索引

二级索引通常不能唯一标识一条记录,而是用来加速特定值的查询。

二级索引带来的主要挑战是它们不能规整的地映射到分区中。有两种主要的方法来支持对二级索引进行分区:基于文档的分区和基于词条的分区。

基于文档分区的二级索引

img

在这种索引方法中,每个分区完全独立,各自维护自己的二级索引,且只负责自己分区内的文档而不关心其他分区中数据。每当需要写数据库时,包括添加,删除或更新文档等,只需要处理包含目标文档 ID 的那一个分区。因此文档分区索引也被称为本地索引,而不是全局索引。

这种查询分区数据库的方陆有时也称为分散/聚集,显然这种二级索引的查询代价高昂。即使采用了并行查询,也容易导致读延迟显著放大。尽管如此,它还是广泛用于实践: MongoDB 、Riak、Cassandra、Elasticsearch 、SolrCloud 和 VoltDB 都支持基于文档分区二级索引。

基于词条的二级索引分区

可以对所有的数据构建全局索引,而不是每个分区维护自己的本地索引。

为避免成为瓶颈,不能将全局索引存储在一个节点上,否则就破坏了设计分区均衡的目标。所以,全局索引也必须进行分区,且可以与数据关键字采用不同的分区策略。

img

可以直接通过关键词来全局划分索引,或者对其取哈希值。直接分区的好处是可以支持高效的区间查询;而采用哈希的方式则可以更均句的划分分区。

这种全局的词条分区相比于文档分区索引的主要优点是,它的读取更为高效,即它不需要采用 scatter/gather 对所有的分区都执行一遍查询。

然而全局索引的不利之处在于, 写入速度较慢且非常复杂,主要因为单个文档的更新时,里面可能会涉及多个二级索引,而二级索引的分区又可能完全不同甚至在不同的节点上,由此势必引人显著的写放大。

理想情况下,索引应该时刻保持最新,即写入的数据要立即反映在最新的索引上。但是,对于词条分区来讲,这需要一个跨多个相关分区的分布式事务支持,写入速度会受到极大的影响,所以现有的数据库都不支持同步更新二级索引。

分区再均衡

随着时间的推移,数据库可能总会出现某些变化:

  • 查询压力增加,因此需要更多的 C PU 来处理负载。
  • 数据规模增加,因此需要更多的磁盘和内存来存储数据。
  • 节点可能出现故障,因此需要其他机器来接管失效的节点。

所有这些变化都要求数据和请求可以从一个节点转移到另一个节点。这样一个迁移负载的过程称为再平衡(或者动态平衡)。无论对于哪种分区方案, 分区再平衡通常至少要满足:

  • 平衡之后,负载、数据存储、读写请求等应该在集群范围更均匀地分布。
  • 再平衡执行过程中,数据库应该可以继续正常提供读写服务。
  • 避免不必要的负载迁移,以加快动态再平衡,井尽量减少网络和磁盘 I/O 影响。

动态再平衡的策略

为什么不用取模?

最好将哈希值划分为不同的区间范围,然后将每个区间分配给一个分区。

为什么不直接使用 mod,对节点数取模方怯的问题是,如果节点数 N 发生了变化,会导致很多关键字需要从现有的节点迁移到另一个节点。

固定数量的分区

创建远超实际节点数的分区数,然后为每个节点分配多个分区。

接下来, 如果集群中添加了一个新节点,该新节点可以从每个现有的节点上匀走几个分区,直到分区再次达到全局平衡。

动态分区

对于采用关键宇区间分区的数据库,如果边界设置有问题,最终可能会出现所有数据都挤在一个分区而其他分区基本为空,那么设定固定边界、固定数量的分区将非常不便:而手动去重新配置分区边界又非常繁琐。

因此, 一些数据库如 HBas e 和 RethinkDB 等采用了动态创建分区。当分区的数据增长超过一个可配的参数阔值( HBase 上默认值是 lOGB ),它就拆分为两个分区,每个承担一半的数据量[26]。相反,如果大量数据被删除,并且分区缩小到某个阑值以下,则将其与相邻分区进行合井。

每个分区总是分配给一个节点,而每个节点可以承载多个分区,这点与固定数量的分区一样。当一个大的分区发生分裂之后,可以将其中的一半转移到其他某节点以平衡负载。

但是,需要注意的是,对于一个空的数据库, 因为没有任何先验知识可以帮助确定分区的边界,所以会从一个分区开始。可能数据集很小,但直到达到第一个分裂点之前,所有的写入操作都必须由单个节点来处理, 而其他节点则处于空闲状态。

按节点比例分区

采用动态分区策略,拆分和合并操作使每个分区的大小维持在设定的最小值和最大值之间,因此分区的数量与数据集的大小成正比关系。另一方面,对于固定数量的分区方式,其每个分区的大小也与数据集的大小成正比。两种情况,分区的数量都与节点数无关。

Cassandra 和 Ketama 则采用了第三种方式,使分区数与集群节点数成正比关系。换句话说,每个节点具有固定数量的分区。此时, 当节点数不变时,每个分区的大小与数据集大小保持正比的增长关系; 当节点数增加时,分区则会调整变得更小。较大的数据量通常需要大量的节点来存储,因此这种方陆也使每个分区大小保持稳定。当一个

新节点加入集群时,它随机选择固定数量的现有分区进行分裂,然后拿走这些分区的一半数据量,将另一半数据留在原节点。随机选择分区边界的前提要求采用基于哈希分区(可以从哈希函数产生的数字范围里设置边界)

自动与手动再平衡操作

全自动式再平衡会更加方便,它在正常维护之外所增加的操作很少。但是,也有可能出现结果难以预测的情况。再平衡总体讲是个比较昂贵的操作,它需要重新路由请求井将大量数据从一个节点迁移到另一个节点。万一执行过程中间出现异常,会使网络或节点的负载过重,井影响其他请求的性能。

将自动平衡与自动故障检测相结合也可能存在一些风险。例如,假设某个节点负载过重,对请求的响应暂时受到影响,而其他节点可能会得到结论:该节点已经失效;接下来激活自动平衡来转移其负载。客观上这会加重该节点、其他节点以及网络的负荷,可能会使总体情况变得更槽,甚至导致级联式的失效扩散。

请求路由

处理策略

  1. 允许客户端链接任意的节点(例如,采用循环式的负载均衡器)。如果某节点恰好拥有所请求的分区,贝 lj 直接处理该请求:否则,将请求转发到下一个合适的节点,接收答复,并将答复返回给客户端。
  2. 将所有客户端的请求都发送到一个路由层,由后者负责将请求转发到对应的分区节点上。路由层本身不处理任何请求,它仅充一个分区感知的负载均衡器。
  3. 客户端感知分区和节点分配关系。此时,客户端可以直接连接到目标节点,而不需要任何中介。

img

许多分布式数据系统依靠独立的协调服务(如 ZooKeeper )跟踪集群范围内的元数据。每个节点都向 ZooKeeper 中注册自己, ZooKeeper 维护了分区到节点的最终映射关系。其他参与者(如路由层或分区感知的客户端)可以向 ZooKeeper 订阅此信息。一旦分区发生了改变,或者添加、删除节点, ZooKeeper 就会主动通知路由层,这样使路由信息保持最新状态。

img

事务

事务中的所有读写是一个执行的整体,整事务要么成功(提交)、要么失败(中止或回滚)。如果失败,应用程序可以安全地重试。

深入理解事务

ACID:原子性( Atomicity ), 一致性( Consistency ),隔离性( Isolation )与持久性( Durability )

若隔离级别

串行化

分布式系统的挑战

所有可能出错的事情一定会出错

分布式系统中可能发生各种问题:

  • 不可靠的网络:当通过网络发送数据包时,数据包可能会丢失或者延迟;同样,回复也可能会丢失或延迟。所以如果没有收到回复,并不能确定消息是否发送成功。
  • 不可靠的时钟:节点的时钟可能会与其他节点存在明显的不同步(尽管尽最大努力设置了 NTP 服务器),时钟还可能会突然向前跳跃或者倒退, 依靠精确的时钟存在一些风险,没有特别简单的办法来精确测量时钟的偏差范围。
  • 进程可能在执行过程中的任意时候遭遇长度未知的暂停( 一个重要的原因是垃圾回收),结果它被其他节点宣告为失效,尽管后来又恢复执行,却对中间的暂停毫无所知。

部分失效可能是分布式系统的关键特征。对于分布式环境,应该具备必要的容错能力。这样即使某些部件发生失效,系统整体还可以继续运行。

为了容忍错误,第一步是检测错误。多数系统没有检测节点是否发生故障的准确机制,因此分布式算法更多依靠超时来确定远程节点是否仍然可用。但是,超时无法区分网络和节点故障,且可变的网络延迟有时会导致节点被误判
为宕机。此外,节点可能处于一种降级状态: 例如,由于驱动程序错误,千兆网络接口可能突然降到 l kb/s 的吞吐量 。这样一个处于“残废”的节点比彻底挂掉的故障节点更难处理。

检测到错误之后,让系统容忍失效也不容易。在典型的分布式环境下,没有全局变量, 没有共享内存,没有约定的尝试或其他跨节点的共享状态。节点甚至不太清楚现在的准确时间, 更不用说其他更高级的了。信息从一个节点流动到另一个节点只能是通过不可靠的网络来发送。单个节点无住安全的做出任何决策,而是需要多个节点之
间的共识协议,井争取达到法定票数(通常为半数以上)。

一致性和共识

分布式系统最重要的抽象之一就是共识: 所有的节点就某一项提议达成一致。

一致性保证

分布式一致性则主要是针对延迟和故障等问题来协调副本之间的状态。

可线性化

在最终一致性数据库中,同时查陶两个不同的副本可能会得到两个不同的答案。

线性化(一种流行的一致性模型) : 其目标是使多副本对外看起来好像是单一副本,然后所有操作以原子方
式运行,就像一个单线程程序操作变量一样。

在有些场景下,线性化对于保证系统正确工作至关重要。

加锁与主节点选举:主从复制的系统需要确保有且只有一个主节点,否则会产生脑裂。选举新的主节点常见的方住是使用锁:即每个启动的节点都试图获得锁,其中只有一个可以成功即成为主节点 。不管锁具体如何实现,它必须满足可线性化:所有节点都必须同意哪个节点持有锁,否则就会出现问题。

约束与唯一性保证:常见如关系型数据库中主键的约束,则需要线性化保证。其他如外键或属性约束,则并不要求一定线性化 。

跨通道的时间依赖

实现线性化系统

线性化本质上意味着“表现得好像只有一个数据副本,且其上的所有操作都是原子的,所以最简单的方案自然是只用一个数据副本。

  • 主从复制(部分支持可线性化):只有主节点承担数据写入,从节点则在各自节点上维护数据的备份副本。如果从主节点或者同步更新的从节点上读取,则可以满足线性化
  • 共识算挂(可线性化):共识协议通常内置一些措施来防止裂脑和过期的副本。
  • 多主复制(不可线性化):具有多主节点复制的系统通常无怯线性化的,主要由于它们同时在多个节点上执行并发写入,并将数据异步复制到其他节点。因此它们可能会产生冲突的写入。
  • 无主复制(可能不可线性化)

线性化的代价

多主复制非常适合多数据中心。

如果两个数据中心之间发生网络中断,会发生什么情况?

基于多主复制的数据库,每个数据中心内都可以继续正常运行: 由于从一个数据中心到另一个数据中心的复制是异步,期间发生的写操作都暂存在本地队列,等网络恢复之后再继续同步。

与之对比,如果是主从复制,则主节点肯定位于其中的某一个数据中心。所有写请求和线性化读取都必须发送给主节点,因此,对于那些连接到非主节点所在数据中心的客户端,读写请求都必须通过数据中心之间的网络,同步发送到主节点所在的数据中。

对于这样的主从复制系统,数据中心之间的网络一旦中断,连接到从数据中心的客户端无怯再联系上主节点,也就无法完成任何数据库写入和线性化读取。从节点可以提供读服务,但内容可能是过期的(非线性化保证)。

CAP 理论

顺序保证

因果关系对事件进行了某种排序(根据事件发生的原因-结果依赖关系)。线性化是将所有操作都放在唯一的、全局有序时间线上,而因果性则不同,它为我们提供了一个弱一致性模型: 允许存在某些井发事件,所以版本历史
是一个包含多个分支与合井的时间线。因果一致性避免了线性化昂贵的协调开销,且对网络延迟的敏感性要低很多。

分布式事务与共识

共识意味着就某一项提议,所有节点做出一致的决定,而且决定不可撤销。

如果系统只存在一个节点, 或者愿意把所有决策功能者都委托给某一个节点,那么事情就变得很简单。这和主从复制数据库的情形是一样的,即由主节点负责所有的决策事直,正因如此,这样的数据库可以提供线性化操作、H 住一性约束、完全有序的复制日志等。

然而,如果唯一的主节点发生故障,或者出现网络中断而导致主节点不可达,这样的系统就会陷入停顿状态。有以下三种基本思路来处理这种情况:

  • 系统服务停止,等待主节点恢复
  • 人为介入选择新主节点,并重新配置系统使之生效
  • 采用算 i 法来自动选择新的主节点。这需要一个共识算法

《数据密集型应用系统设计》笔记二之数据系统基础

第 1 章 可靠、可扩展与可维护的应用系统

认识数据系统

很多应用系统都包含以下数据处理系统:

  • 数据库:用以存储数据,这样之后应用可以再次面问。
  • 高速缓存: 缓存那些复杂或操作代价昂贵的结果,以加快下一次访问。
  • 索引: 用户可以按关键字搜索数据井支持各种过掳。
  • 流式处理:持续发送消息至另一个进程,处理采用异步方式。
  • 批处理: 定期处理大量的累积数据。

设计数据系统或数据服务时,需要考虑很多因素,其中最重要的三个问题:

  • 可靠性(Reliability):当出现意外情况如硬件、软件故障、人为失误等,系统应可以继续正常运转:虽然性能可能有所降低,但确保功能正确。
  • 可扩展性(Scalability):随着规模的增长,例如数据量、流量或复杂性,系统应以合理的方式来匹配这种增长。
  • 可维护性(Maintainability):随着时间的推移,许多新的人员参与到系统开发和运维, 以维护现有功能或适配新场景等,系统都应高效运转。

可靠性

可靠性意味着:即时发生了某些错误,系统仍然可以继续正常工作。

系统可应对错误则称为容错(fault tolerant)或者弹性(resilient)。

常见的故障类型:

  • 硬件故障:通常是随机的,如:硬盘崩溃、内存故障、电网停电、断网等。常见应对策略:使用集群去冗余。
  • 软件故障:各种难以预料的 bug。
  • 人为故障:如操作不当。

可扩展性

可扩展性是指负载增加时, 有效保持系统性能的相关技术策略。

吞吐量:每秒可处理的记录数

响应时间:中位数指标比平均响应时间更适合描述等待时间。

如何应对负载:垂直扩展(升级硬件)和水平扩展(集群、分布式)

可维护性

  • 可运维性:方便运营团队来保持系统平稳运行。
  • 简单性:简化系统复杂性,使新工程师能够轻松理解系统。
  • 可演化性:后续工程师能够轻松地对系统进行改进,井根据需求变化将其适配到非典型场景,也称为可延伸性、易修改性或可塑性。

主要措施:

  • 良好的抽象可以帮助降低复杂性, 井使系统更易于修改和适配新场景。
  • 良好的可操作性意味着对系统健康状况有良好的可观测性和有效的管理方战。

第 2 章 数据模型与查询语言

复杂的应用程序可能会有更多的中间层,每层都通过提供一个简洁的数据模型来隐藏下层的复杂性。

如果数据大多是一对多关系(树结构数据)或者记录之间没有关系,那么文档模型是最合适的。

关系模型能够处理简单的多对多关系,但是随着数据之间的关联越来越复杂,将数据建模转化为图模型会更加自然。

第 3 章 数据存储与检索

从最基本的层面看,数据库只需做两件事情:存储和检索。

数据库核心:数据结构

为了高效地查找数据库中特定键的值, 需要新的数据结构: 索引。

存储系统的设计权衡:适当的索引可以加速读取查询,但每个索引都会减慢写速度。数据库通常不会对所有内容进行索引。

索引类型:

  • 哈希索引
  • B+ 树
  • LSM 树
  • 等等

扩展阅读:检索技术核心 20 讲

事务处理与分析处理

列式存储

如果表中有数以万亿行、PB 大小的数据,则适合用于存储在列式存储中。

第 4 章 数据编码与演化

本章节主要介绍各种序列化、反序列化方式。略