Dunwu Blog

大道至简,知易行难

分布式综合面试

逻辑时钟

::: info 扩展阅读

:::

【简单】为什么需要逻辑时钟?

::: tip 要点

不同节点的物理时钟即使校准(NTP)也无法完全保持一致

:::

为什么需要逻辑时钟?分布式系统中以系统时间来确定事件顺序有什么问题吗?

不同节点的物理时钟无法完全保持一致。即使引入一个全局时钟(例如:NTP)来进行校准,由于网络通信延迟的不确定性,以及时钟计时的偏差,无法保证每个节点的时间完全一致。

在分布式系统中,由于网络通信延迟的不确定性, 仅仅以接收顺序作为整个分布式系统中事件的发生顺序是不可取的

【中等】什么是偏序?什么是全序?

全序和偏序是数学上的术语,按照数学内容阐述比较晦涩,简单来说:

  • 偏序是部分可比较的有序关系。
  • 全序是在偏序基础上,要求全部元素必须可比较的有序关系。

【困难】什么是逻辑时钟?

::: info 扩展阅读

Time, Clocks, and the Ordering of Events in a Distributed System

:::

::: tip 要点

  • Lamport 逻辑时钟构建了一个全序时钟来描述事件顺序
  • Lamport 逻辑时钟的缺陷是无法描述同时发生的事件

:::

1978 年,Lamport 在 Time, Clocks, and the Ordering of Events in a Distributed System 中提出了逻辑时钟的概念,来解决分布式系统中区分事件发生的时序问题。

逻辑时钟并不度量时间本身,仅区分事件发生的前后顺序

分布式系统中按是否存在节点交互可分为三类事件,一类发生于节点内部,二是发送事件,三是接收事件。Lamport 时间戳原理如下:

Lamport timestamps space time (图片来源: wikipedia)_

  1. 每个事件对应一个 Lamport 计数器,初始值为 0
  2. 如果事件在节点内发生,计数器加 1
  3. 如果事件属于发送事件,计数器加 1 并在消息中带上该计数器
  4. 如果事件属于接收事件,计数器 = Max(本地计数器,消息中的计数器) + 1

综上,Lamport 逻辑时钟构建了一个全序时钟来描述事件顺序Lamport 逻辑时钟的缺陷是无法描述同时发生的事件

【困难】什么是向量时钟?

::: tip 要点

  • 向量时钟在逻辑时钟基础上改进:不仅记录了本节点的时间戳,还记录了其他节点的时间戳
  • 其本质在于将逻辑时钟的全序计数器改造为向量时钟的偏序大小关系:向量有序,则事件有序;向量平行,则事件并发。
  • 向量时钟可以发现数据冲突,但不能解决数据冲突

:::

向量时钟其实是在逻辑时钟的基础上进行了演进,算法逻辑类似,只是不仅记录了本节点的时间戳,还记录了其他节点的时间戳。其本质在于将逻辑时钟的全序计数器改造为向量时钟的偏序大小关系:向量有序,则事件有序;向量平行,则事件并发。

Vector clock space time (图片来源: wikipedia)

向量时钟可以发现数据冲突,但不能解决数据冲突

【困难】什么是版本向量时钟?

::: tip 要点

版本向量时钟只有在更新数据的时候做向量自增

:::

在向量时钟算法中, 消息传播后,发送方的向量一定会小于接收者的向量, 是因为接收者对齐了发送者的原因。

版本向量在此基础上,做了一点加强:消息传播后,发送方也对齐接收者的向量,也就是双向对齐,在版本向量中,叫做同步

发送消息和接收消息的时候不再自增向量中的自己的计数器,而是只做双方的向量对齐操作。 也就是,只有在更新数据的时候做向量自增

一致性

【简单】什么是强一致性?什么是弱一致性?什么是最终一致性?

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

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

  • 强一致性 - 数据更新操作结果和操作响应总是一致的,即操作响应通知更新失败,那么数据一定没有被更新,而不是处于不确定状态。通俗的说,分布式系统在执行写操作成功后,如果所有用户都能够读取到最新的值,该系统就被认为具有强一致性。
  • 弱一致性 - 系统在写入数据成功后,不承诺立即能读到最新的值,也不承诺什么时候能读到,但是过一段时间之后用户可以看到更新后的值。那么用户读不到最新数据的这段时间被称为“不一致窗口时间”。
  • 最终一致性 - 最终一致性作为弱一致性中的特例,强调的是所有数据副本,在经过一段时间的同步后,最终能够到达一致的状态,不需要实时保证系统数据的强一致性。

【简单】什么是 ACID?

那么,什么是 ACID 特性呢?ACID 是数据库事务正确执行的四个基本要素的单词缩写:

  • 原子性(Atomicity)
    • 原子是指不可分解为更小粒度的东西。事务的原子性意味着:事务中的所有操作要么全部成功,要么全部失败
    • 回滚可以用日志来实现,日志记录着事务所执行的修改操作,在回滚时反向执行这些修改操作即可。
    • ACID 中的原子性并不关乎多个操作的并发性,它并没有描述多个线程试图访问相同的数据会发生什么情况,后者其实是由 ACID 的隔离性所定义。
  • 一致性(Consistency)
    • 数据库在事务执行前后都保持一致性状态。
    • 在一致性状态下,所有事务对一个数据的读取结果都是相同的。
    • 一致性本质上要求应用层来维护状态一致(或者恒等),应用程序有责任正确地定义事务来保持一致性。这不是数据库可以保证的事情。
  • 隔离性(Isolation)
    • 同时运行的事务互不干扰。换句话说,一个事务所做的修改在最终提交以前,对其它事务是不可见的。
  • 持久性(Durability)
    • 一旦事务提交,则其所做的修改将会永远保存到数据库中。即使系统发生崩溃,事务执行的结果也不能丢失。
    • 可以通过数据库备份和恢复来实现,在系统发生奔溃时,使用备份的数据库进行数据恢复。

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

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

CAP & BASE

::: info 扩展阅读

:::

【中等】什么是 CAP 定理?

::: tip 要点

  • CAP 就是取 Consistency、Availability、Partition Tolerance 的首字母而命名。
  • CAP 定理提出:Consistency、Availability、Partition Tolerance 三者不可兼得。
  • 在分布式系统中,分区容错不可避免,因此,CAP 定理实际上是要在可用性(A)和一致性(C)之间做权衡

:::

CAP 定理提出:分布式系统有三个指标,这三个指标不能同时做到:

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

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

在分布式系统中,分区容错性是一个既定的事实:因为分布式系统总会出现各种各样的问题,如由于网络原因而导致节点失联;发生机器故障;机器重启或升级等等。因此,CAP 定理实际上是要在可用性(A)和一致性(C)之间做权衡

【中等】选择 CP 还是 AP?

::: tip 要点

选择 AP 还是 CP,视具体业务场景而定。

以注册中心而论,可用性比一致性更重要,选 AP 更合适。

:::

在分布式系统中,分区容错性是一个既定的事实:因为分布式系统总会出现各种各样的问题,如由于网络原因而导致节点失联;发生机器故障;机器重启或升级等等。因此,CAP 定理实际上是要在可用性(A)和一致性(C)之间做权衡

  • 选择 AP 模式,偏向于保证服务的高可用性。用户访问系统的时候,都能得到响应数据,不会出现响应错误;但是,当出现分区故障时,相同的读操作,访问不同的节点,得到响应数据可能不一样。
  • 选择 CP 模式,一旦因为消息丢失、延迟过高发生了网络分区,就会影响用户的体验和业务的可用性。因为为了防止数据不一致,系统将拒绝新数据的写入。

一个最具代表性的问题是:服务注册中心应该选择 AP 还是 CP?

在微服务架构下,服务注册和服务发现机制中主要有三种角色:

  • 服务提供者(RPC Server / Provider)
  • 服务消费者(RPC Client / Consumer)
  • 服务注册中心(Registry)

注册中心负责协调服务注册和服务发现,显然它是核心中的核心。主流的注册中心有很多,如:ZooKeeper、Nacos、Eureka、Consul、etcd 等。在针对注册中心进行技术选型时,其 CAP 设计也是一个比较的维度。

  • CP 模型代表:ZooKeeper、etcd。系统强调数据的一致性,当数据一致性无法保证时(如:正在选举主节点),系统拒绝请求。
  • AP 模型代表:Nacos、Eureka。系统强调可用性,牺牲一定的一致性(即服务节点上的数据不保证是最新的),来保证整体服务可用。

对于服务注册中心而言,即使不同节点保存的服务注册信息存在差异,也不会造成灾难性的后果,仅仅是信息滞后而已。但是,如果为了追求数据一致性,使得服务发现短时间内不可用,负面影响更严重。所以,对于服务注册中心而言,可用性比一致性更重要,一般应该选择 AP 模型。

【中等】CAP 定理真的正确吗?

::: tip 要点

CAP 理论模型局限性很大,未考虑网络分区以外的各种故障、异常情况,因此对于复杂的分布式系统场景指导意义不足。

:::

CAP 定理在分布式系统领域大名鼎鼎,以至于被很多人视为了真理。然而,CAP 定理真的正确吗?

网络分区是一种故障,不管喜欢还是不喜欢,它都可能发生,所以无法选择或逃避分区的问题。在网络正常的时候,系统可以同时保证一致性(线性化)和可用性。而一旦发生了网络故障,必须要么选择一致性,要么选择可用性。因此,对 CAP 更准确的理解应该是:当发生网络分区(P)的情况下,可用性(A)和一致性(C)二者只能选其一

CAP 定理所描述的模型实际上局限性很大,它只考虑了一种一致性模型和一种故障(网络分区故障),而没有考虑网络延迟、节点失效等情况。因此,它对于指导一个具体的分布式系统设计来说,没有太大的实际价值。

值得一提的是,在 CAP 定理提出十二年之后,其提出者也发表了一篇文章 CAP Twelve Years Later: How the “Rules” Have Changed,来阐述 CAP 定理的局限性。

【中等】什么是 BASE 定理?

::: tip 要点

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

BASE 核心思想是:要求最终一致性,通过牺牲强一致性来达到可用性。

:::

BASE 是 基本可用(Basically Available)软状态(Soft State)最终一致性(Eventually Consistent) 三个短语的缩写。BASE 定理是对 CAP 定理中可用性(A)和一致性(C)权衡的结果。

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

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

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

Paxos

::: info 扩展阅读

:::

【困难】Paxos 是怎样工作的?

::: tip 要点

Paxos 是一个分布式系统共识性算法。它的核心思想是【两阶段提交】【多数派决议】

Paxos 将分布式系统中的节点分 Proposer、Acceptor、Learner 三种角色。

Paxos 算法包含 2 个部分:

  • Basic Paxos 算法:描述的是多节点之间如何就某个值达成共识。Basic Paxos 是通过二阶段提交的方式来达成共识的
  • Multi Paxos 思想:描述的是执行多个 Basic Paxos 实例,就一系列值达成共识。

:::

Paxos 是一种基于消息传递且具有容错性的共识性(consensus)算法

Paxos 算法运行在允许宕机故障的异步系统中,不要求可靠的消息传递,可容忍消息丢失、延迟、乱序以及重复。

Paxos 利用多数派 (Majority) 机制保证了一定的容错能力,即 N 个节点的系统最多允许 N / 2 - 1 个节点同时出现故障。

Paxos 算法包含 2 个部分:

  • Basic Paxos 算法:描述的是多节点之间如何就某个值达成共识。
  • Multi Paxos 思想:描述的是执行多个 Basic Paxos 实例,就一系列值达成共识。

Basic Paxos 算法

Basic Paxos 是通过二阶段提交的方式来达成共识的

Paxos 将分布式系统中的节点分 Proposer、Acceptor、Learner 三种角色。

  • 提议者(Proposer):发出提案(Proposal),用于投票表决。Proposal 信息包括提案编号 (Proposal ID) 和提议的值 (Value)。在绝大多数场景中,集群中收到客户端请求的节点,才是提议者。这样做的好处是,对业务代码没有入侵性,也就是说,我们不需要在业务代码中实现算法逻辑。
  • 接受者(Acceptor):对每个 Proposal 进行投票,若 Proposal 获得多数 Acceptor 的接受,则称该 Proposal 被批准。一般来说,集群中的所有节点都在扮演接受者的角色,参与共识协商,并接受和存储数据。
  • 学习者(Learner):不参与接受,从 Proposers/Acceptors 学习、记录最新达成共识的提案(Value)。一般来说,学习者是数据备份节点,比如主从架构中的从节点,被动地接受数据,容灾备份。

Paxos 算法有 3 个阶段,其中,前 2 个阶段负责协商并达成共识:

  1. 准备(Prepare)阶段:Proposer 向 Acceptors 发出 Prepare 请求,Acceptors 针对收到的 Prepare 请求进行 Promise 承诺。
  2. 接受(Accept)阶段:Proposer 收到多数 Acceptors 承诺的 Promise 后,向 Acceptors 发出 Propose 请求,Acceptors 针对收到的 Propose 请求进行 Accept 处理。
  3. 学习(Learn)阶段:Proposer 在收到多数 Acceptors 的 Accept 之后,标志着本次 Accept 成功,决议形成,将形成的决议发送给所有 Learners。

Multi Paxos 思想

Basic Paxos 有以下问题,导致它不能应用于实际:

  • Basic Paxos 算法只能对一个值形成决议
  • Basic Paxos 算法会消耗大量网络带宽。Basic Paxos 中,决议的形成至少需要两次网络通信,在高并发情况下可能需要更多的网络通信,极端情况下甚至可能形成活锁。如果想连续确定多个值,Basic Paxos 搞不定了。

Multi Paxos 基于 Basic Paxos 做了两点改进:

  • 针对每一个要确定的值,运行一次 Paxos 算法实例(Instance),形成决议。每一个 Paxos 实例使用唯一的 Instance ID 标识。
  • 在所有 Proposer 中选举一个 Leader,由 Leader 唯一地提交 Proposal 给 Acceptor 进行表决。这样没有 Proposer 竞争,解决了活锁问题。在系统中仅有一个 Leader 进行 Value 提交的情况下,Prepare 阶段就可以跳过,从而将两阶段变为一阶段,提高效率。

Raft

::: info 扩展阅读

:::

【困难】Raft 是怎样工作的?

::: tip 要点

Raft 是一种管理日志复制的分布式共识性算法

Raft 将一致性问题分解成了三个子问题:

  • 选举 Leader
    • Leader 心跳:Leader 定时向 Follower 发心跳以续活;Follower 超时未收到心跳,视其为下线。
    • 投票选举:Follower 判断 Leader 下线后,发起 Leader 选举,成为 Candidate。
    • 多数派原则:得到大多数选票的 Candidate 当选为 Leader。
    • 随机的竞选超时时间
      • 每个 Follower 都设置一个随机的竞选超时时间,该时间范围内,未收到 Leader 的心跳,就视为当前 Term 无 Leader,再次 Leader 选举。
      • 之所以是随机时间,是为了避免重复出现相同投票结果,导致始终选不出 Leader 的情况(一种典型的活锁)。
  • 日志复制:Leader 负责处理所有客户端读写请求;Follower 只负责同步 Leader 的日志,并更新本地的日志状态机(Offset)
  • 安全性
    • 选举限制:拥有最新的已提交的日志条目的 Follower 才有资格成为 Leader。
    • 提交旧任期的日志条目Raft 永远不会通过计算副本数目的方式去提交一个之前 Term 内的日志条目
    • 日志压缩:Raft 采用对整个系统进行快照来解决,快照之前的日志都可以丢弃。以此,避免日志无限膨胀,导致故障恢复过久。

:::

Raft 是一种管理日志复制的分布式共识性算法。从本质上说,Raft 算法是通过一切以领导者为准的方式,实现一系列值的共识和各节点日志的一致

Raft 出现之前,Paxos 一直是分布式共识性算法的标准。Paxos 难以理解,更难以实现。Raft 的设计目标是简化 Paxos,使得算法既容易理解,也容易实现

Raft 将一致性问题分解成了三个子问题:

  • 选举 Leader
  • 日志复制
  • 安全性

Raft 概念

(1)服务器角色

在 Raft 中,任何时刻,每个服务器都处于这三个角色之一 :

  • Leader - 领导者,通常一个系统中是一主(Leader)多从(Follower)。Leader 负责处理所有的客户端请求
  • Follower - 跟随者,不会发送任何请求,只是简单的 响应来自 Leader 或者 Candidate 的请求
  • Candidate - 参选者,选举新 Leader 时的临时角色。

(2)任期

Raft 把时间分割成任意长度的 任期(Term),任期用连续的整数标记。每一段任期从一次选举开始。Raft 保证了在一个给定的任期内,最多只有一个领导者

任期在 Raft 算法中充当逻辑时钟的作用,使得服务器节点可以查明一些过期的信息(比如过期的 Leader)。每个服务器节点都会存储一个当前任期号,这一编号在整个时期内单调的增长。当服务器之间通信的时候会交换当前任期号。

(3)选举 Leader

领导者心跳消息:Raft 使用一种心跳机制来触发 Leader 选举。Leader 需要周期性的向所有 Follower 发送心跳消息,以此维持 Leader 身份。

随机的竞选超时时间:每个 Follower 都设置了一个随机的竞选超时时间,一般为 150ms ~ 300ms,如果在竞选超时时间内没有收到 Leader 的心跳消息,就会认为当前 Term 没有可用的 Leader,并发起选举来选出新的 Leader。开始一次选举过程,Follower 先要增加自己的当前 Term 号,并转换为 Candidate

Candidate 会并行的向集群中的所有服务器节点发送投票请求(RequestVote RPC,它会保持当前状态直到以下三件事情之一发生:

  • 自己成为 Leader
  • 其他的服务器成为 Leader
  • 没有任何服务器成为 Leader

Raft 算法通过:领导者心跳消息、随机选举超时时间、得到大多数选票才通过原则、任期最新者优先、先来先服务等投票原则,保证了一个任期只有一位领导,也极大地减少了选举失败的情况。

日志复制

  1. Leader 负责处理所有客户端的请求。
  2. Leader 把请求作为日志条目加入到它的日志中,然后并行的向其他服务器发送 AppendEntries RPC 请求,要求 Follower 复制日志条目。
  3. Follower 复制成功后,返回确认消息。
  4. 当这个日志条目被半数以上的服务器复制后,Leader 提交这个日志条目到它的复制状态机,并向客户端返回执行结果。

安全性

  • 选举限制:拥有最新的已提交的日志条目的 Follower 才有资格成为 Leader。
  • 提交旧任期的日志条目Raft 永远不会通过计算副本数目的方式去提交一个之前 Term 内的日志条目
  • 日志压缩:Raft 采用对整个系统进行快照来解决,快照之前的日志都可以丢弃。以此,避免日志无限膨胀,导致故障恢复过久。

ZAB

::: info 扩展阅读

:::

【困难】ZAB 是怎样工作的?

::: tip 要点

ZAB 协议是 Zookeeper 专门设计的一种支持故障恢复的原子广播协议

ZAB 协议定义了两个可以无限循环的流程:

  • 选举 Leader:用于故障恢复,从而保证高可用。采用多数派原则选举 Leader。
  • **原子广播**:用于主从同步,从而保证数据一致性。
    • Leader 负责所有读写;每次更新事务会有一个唯一标识(ZXID
    • Leader 会广播同步数据给 Follower,当半数以上 Follower 更新成功,Leader 才会提交数据。

:::

ZAB 协议是 Zookeeper 专门设计的一种支持故障恢复的原子广播协议

ZAB 协议是 ZooKeeper 的数据一致性和高可用解决方案。

ZAB 协议定义了两个可以无限循环的流程:

  • 选举 Leader - 用于故障恢复,从而保证高可用。
  • 原子广播 - 用于主从同步,从而保证数据一致性。

选举 Leader

ZooKeeper 集群采用一主(称为 Leader)多从(称为 Follower)模式,主从节点通过副本机制保证数据一致。

  • 如果 Follower 节点挂了 - ZooKeeper 集群中的每个节点都会单独在内存中维护自身的状态,并且各节点之间都保持着通讯,只要集群中有半数机器能够正常工作,那么整个集群就可以正常提供服务
  • 如果 Leader 节点挂了 - 如果 Leader 节点挂了,系统就不能正常工作了。此时,需要通过 ZAB 协议的选举 Leader 机制来进行故障恢复。

ZAB 协议的选举 Leader 机制简单来说,就是:基于过半选举机制产生新的 Leader,之后其他机器将从新的 Leader 上同步状态,当有过半机器完成状态同步后,就退出选举 Leader 模式,进入原子广播模式。

原子广播

ZooKeeper 通过副本机制来实现高可用

那么,ZooKeeper 是如何实现副本机制的呢?答案是:ZAB 协议的原子广播。

ZAB 协议的原子广播要求:

**所有的写请求都会被转发给 Leader,Leader 会以原子广播的方式通知 Follow。当半数以上的 Follow 已经更新状态持久化后,Leader 才会提交这个更新,然后客户端才会收到一个更新成功的响应**。这有些类似数据库中的两阶段提交协议。

在整个消息的广播过程中,Leader 服务器会每个事务请求生成对应的 Proposal,并为其分配一个全局唯一的递增的事务 ID(ZXID),之后再对其进行广播。

Gossip

【困难】Gossip 是怎样工作的?

::: info 扩展阅读

:::

::: tip 要点

Gossip 是一种用于分布式系统节点间信息交换的协议。

Gossip 的设计思想基于去中心化最终一致性

Gossip 协议的工作原理:

  • 周期性、成对通信:每个节点每隔一段时间就随机选择集群中的另一个节点(这个节点称为“邻居”)。
  • 交换信息:两个节点连接后,会互相交换自己拥有的信息(例如,其他节点的状态、存储的数据等)。
  • 感染式传播:接收到新信息的节点,会将这些新信息融入到自己的信息库中。在下一次周期中,它又会成为传染源,将(包含新信息的)所有信息再次传播给其他随机节点。
  • 最终一致性:不需要中央协调,经过一段时间后,通过这种“八卦”式的传播,集群中的所有节点最终都会拥有完全相同的信息。

:::

Gossip 也叫 Epidemic Protocol (流行病协议),这个协议基于最终一致性以及去中心化设计思想。主要用于分布式节点之间进行信息交换和数据同步,这种场景的一个最大特点就是组成的网络的节点都是对等节点,是非结构化网络(去中心化)。

Gossip 过程是由种子节点发起,当一个种子节点有状态需要更新到网络中的其他节点时,它会随机的选择周围几个节点散播消息,收到消息的节点也会重复该过程,直至最终网络中所有的节点都收到了消息。这个过程可能需要一定的时间,由于不能保证某个时刻所有节点都收到消息,但是理论上最终所有节点都会收到消息,因此它是一个最终一致性协议。

Gossip 过程是异步的,也就是说发消息的节点不会关注对方是否收到,即不等待响应;不管对方有没有收到,它都会每隔 1 秒向周围节点发消息。异步是它的优点,而消息冗余则是它的缺点

Goosip 协议的信息传播和扩散通常需要由种子节点发起。整个传播过程可能需要一定的时间,由于不能保证某个时刻所有节点都收到消息,但是理论上最终所有节点都会收到消息,因此它是一个最终一致性协议。

Gossip 有两种类型:

  • Anti-Entropy(反熵)以固定的概率传播所有的数据。反熵时通讯成本会很高,可以通过引入校验和等机制,降低需要对比的数据量和通讯消息等。反熵不适合动态变化或节点数比较多的分布式环境。
  • Rumor-Mongering(谣言传播)仅传播新到达的数据。谣言传播模型指的是当一个节点有了新数据后,这个节点变成活跃状态,并周期性地联系其他节点向其发送新数据,直到所有的节点都存储了该新数据。在谣言传播模型下,消息可以发送得更频繁,因为消息只包含最新 update,体积更小。而且,一个谣言消息在某个时间点之后会被标记为 removed,并且不再被传播,因此,谣言传播模型下,系统有一定的概率会不一致。而由于,谣言传播模型下某个时间点之后消息不再传播,因此消息是有限的,系统开销小。

分布式简介

分布式系统的发展历程

罗马不是一天建成的,同理,现代分布式系统架构也不是一蹴而就的,而是逐步发展的演化过程。随着业务的不断发展,用户体量的增加,系统的复杂度势必不断攀升,最终迫使系统架构进化,以应对挑战。

了解分布式系统架构的演化过程,有利于我们了解架构进化的发展规律和业界一些成熟的应对方案。帮助我们在实际工作中,如何去思考架构,如何去凝练解决方案。

单机架构

  • 场景:网站运营初期,访问用户少,一台服务器绰绰有余。
  • 特征应用程序、数据库、文件等所有的资源都在一台服务器上。
  • 描述:通常服务器操作系统使用 linux,应用程序使用 PHP 开发,然后部署在 Apache 上,数据库使用 Mysql,通俗称为 LAMP。汇集各种免费开源软件以及一台廉价服务器就可以开始系统的发展之路了。

应用服务和数据服务分离

  • 场景:越来越多的用户访问导致性能越来越差,越来越多的数据导致存储空间不足,一台服务器已不足以支撑。
  • 特征应用服务器、数据库服务器、文件服务器分别独立部署。
  • 描述:三台服务器对性能要求各不相同:
    • 应用服务器要处理大量业务逻辑,因此需要更快更强大的 CPU;
    • 数据库服务器需要快速磁盘检索和数据缓存,因此需要更快的硬盘和更大的内存;
    • 文件服务器需要存储大量文件,因此需要更大容量的硬盘。

使用缓存改善性能

  • 场景:随着用户逐渐增多,数据库压力太大导致访问延迟。
  • 特征:由于网站访问和财富分配一样遵循二八定律:_80% 的业务访问集中在 20% 的数据上_。将数据库中访问较集中的少部分数据缓存在内存中,可以减少数据库的访问次数,降低数据库的访问压力。
  • 描述:缓存分为两种:应用服务器上的本地缓存和分布式缓存服务器上的远程缓存。
    • 本地缓存访问速度更快,但缓存数据量有限,同时存在与应用程序争用内存的情况。
    • 分布式缓存可以采用集群方式,理论上可以做到不受内存容量限制的缓存服务。

负载均衡

  • 场景:使用缓存后,数据库访问压力得到有效缓解。但是单一应用服务器能够处理的请求连接有限,在访问高峰期,成为瓶颈。
  • 特征多台服务器通过负载均衡同时向外部提供服务,解决单一服务器处理能力和存储空间不足的问题。
  • 描述:使用集群是系统解决高并发、海量数据问题的常用手段。通过向集群中追加资源,提升系统的并发处理能力,使得服务器的负载压力不再成为整个系统的瓶颈。

数据库读写分离

  • 场景:网站使用缓存后,使绝大部分数据读操作访问都可以不通过数据库就能完成,但是仍有一部分读操作和全部的写操作需要访问数据库,在网站的用户达到一定规模后,数据库因为负载压力过高而成为网站的瓶颈。
  • 特征:目前大部分的主流数据库都提供主从热备功能,通过配置两台数据库主从关系,可以将一台数据库服务器的数据更新同步到一台服务器上。网站利用数据库的主从热备功能,实现数据库读写分离,从而改善数据库负载压力。
  • 描述:应用服务器在写操作的时候,访问主数据库,主数据库通过主从复制机制将数据更新同步到从数据库。这样当应用服务器在读操作的时候,访问从数据库获得数据。为了便于应用程序访问读写分离后的数据库,通常在应用服务器端使用专门的数据访问模块,使数据库读写分离的对应用透明。

多级缓存

  • 场景:中国网络环境复杂,不同地区的用户访问网站时,速度差别也极大。
  • 特征采用 CDN 和反向代理加快系统的静态资源访问速度。
  • 描述:CDN 和反向代理的基本原理都是缓存,区别在于:
    • CDN 部署在网络提供商的机房,使用户在请求网站服务时,可以从距离自己最近的网络提供商机房获取数据;
    • 而反向代理则部署在网站的中心机房,当用户请求到达中心机房后,首先访问的服务器时反向代理服务器,如果反向代理服务器中缓存着用户请求的资源,就将其直接返回给用户。

业务拆分

  • 场景:大型网站的业务场景日益复杂,分为多个产品线。
  • 特征:采用分而治之的手段将整个网站业务分成不同的产品线。系统上按照业务进行拆分改造,应用服务器按照业务区分进行分别部署。
  • 描述:应用之间可以通过超链接建立关系,也可以通过消息队列进行数据分发,当然更多的还是通过访问同一个数据存储系统来构成一个关联的完整系统。
    • 纵向拆分将一个大应用拆分为多个小应用,如果新业务较为独立,那么就直接将其设计部署为一个独立的 Web 应用系统。纵向拆分相对较为简单,通过梳理业务,将较少相关的业务剥离即可。
    • 横向拆分将复用的业务拆分出来,独立部署为分布式服务,新增业务只需要调用这些分布式服务横向拆分需要识别可复用的业务,设计服务接口,规范服务依赖关系。

分库分表

  • 场景:随着大型网站业务持续增长,数据库经过读写分离,从一台服务器拆分为两台服务器,依然不能满足需求。
  • 特征数据库采用分布式数据库。
  • 描述:分布式数据库是数据库拆分的最后方法,只有在单表数据规模非常庞大的时候才使用。不到不得已时,更常用的数据库拆分手段是业务分库,将不同的业务数据库部署在不同的物理服务器上。

分布式组件

  • 场景:随着网站业务越来越复杂,对数据存储和检索的需求也越来越复杂。
  • 特征系统引入 NoSQL 数据库及搜索引擎。
  • 描述:NoSQL 数据库及搜索引擎对可伸缩的分布式特性具有更好的支持。应用服务器通过统一数据访问模块访问各种数据,减轻应用程序管理诸多数据源的麻烦。

微服务

  • 场景:随着业务越拆越小,存储系统越来越庞大,应用系统整体复杂程度呈指数级上升,部署维护越来越困难。由于所有应用要和所有数据库系统连接,最终导致数据库连接资源不足,拒绝服务。
  • 特征公共业务提取出来,独立部署。由这些可复用的业务连接数据库,通过分布式服务提供共用业务服务。
  • 描述:大型网站的架构演化到这里,基本上大多数的技术问题都得以解决,诸如跨数据中心的实时数据同步和具体网站业务相关的问题也都可以组合改进现有技术架构来解决。

分布式指标

分布式系统的目标是提升系统的整体性能和吞吐量,另外还要尽量保证分布式系统的容错性

由分布式系统的目标很容易得出分布式系统的关键指标:性能、可用性、可扩展性。这些指标,正对应着耳熟能详的分布式系统“三高”特性——高并发、高性能、高可用。

性能(Performance)

性能用于衡量一个系统处理各种任务的能力。

常见的性能指标有:

  • 吞吐量(Throughput) - 系统在一定时间内可以处理的任务数。常见的吞吐量指标有:
    • QPS - Queries Per Second 的缩写,即每秒查询数。
    • TPS - Transactions Per Second 的缩写,即每秒事务数。
  • 响应时间(Response Time) - 执行一个请求从开始到最后收到响应数据所花费的总体时间,即从客户端发起请求到收到服务器响应结果的时间。
  • 并发数(Concurrency) - 并发数是指系统能同时处理请求的数量,这个也反映了系统的负载能力。并发意味着可以同时进行多个处理。并发在现代编程中无处不在,网络中有多台计算机同时存在,一台计算机上同时运行着多个应用程序。

以上三个指标的关系大致为:

1
2
QPS(TPS)= 并发数 / 平均响应时间
并发数 = QPS(TPS) * 平均响应时间

可用性(Availability)

可用性:指的是系统在面对各种异常时可以正确提供服务的能力。

系统的可用性可以用系统停止服务的时间与总的时间之比衡量。

行业内一般用几个 9 表示可用性指标,对应用的可用性程度一般衡量标准有三个 9 到五个 9;一般我们的系统至少要到 4 个 9(99.99%)的可用性才能谈得上高可用。

可用性 年故障时间
99.9999% 32 秒
99.999% 5 分 15 秒
99.99% 52 分 34 秒
99.9% 8 小时 46 分
99% 3 天 15 小时 36 分

而所谓的高可用,就是:在任何情况下,让服务尽最大可能对外提供服务

可扩展性(Scalability)

可扩展性(Scalability)指的是分布式系统通过扩展集群机器规模提高系统性能 (吞吐、响应时间、 完成时间)、存储容量、计算能力的特性,是分布式系统的特有性质。

系统扩展可以分为垂直扩展、水平扩展。

  • 垂直扩展,即提升单机的硬件处理能力,比如 CPU 处理能力,内存容量,磁盘等方面。但是,单机是有性能瓶颈的,一旦触及瓶颈,再想提升,付出的成本和代价会极高。通俗来说,就三个字:得加钱
  • 水平扩展:采用分而治之的思想,通过集群来分担吞吐量。集群中的应用机器(节点)通常被设计成无状态,用户可以请求任何一个节点,这些节点共同分担访问压力。水平扩展有两个要点:
    • 集群化、分区化:将一个完整的应用化整为零,如果是无状态应用,可以直接集群化部署;如果是有状态应用,可以将状态数据分区(分片),然后部署到多台机器上。
    • 负载均衡:集群化、分区化后,要解决的问题是,请求应该被分发(寻址)到哪台机器上。这就需要通过某种策略来控制分发,这种技术就是负载均衡。

分布式系统分类

分布式技术错综复杂、知识庞杂,且各种技术相互耦合,所以不容易划分层次。

从应用的维度来看,大致可以将分布式系统分为以下四类:

  • 分布式计算:解决应用的分布式计算问题。基于分布式计算模式,包括批处理计算、离线计算、在线计算、融合计算等,根据应用类型构建高效智能的分布式计算框架。
  • 分布式存储:解决数据的分布式和多元化问题。包括分布式数据库、分布式文件系统、分布式缓存等,支持不同类型的数据的存储和管理。
  • 分布式通信:解决进程间的分布式通信问题。通过消息队列、远程调用等方式,实现简单高效的通信。
  • 分布式资源管理:解决资源的分布式和异构性问题。将 CPU、内存、IO 等物理资源虚拟化,新城逻辑资源池,以便统一管理。

此外,分布式系统都需要面对一些共性问题,可以视为分布式系统技术的基石:

  • 分布式协同 - 解决分布式状态及数据一致性的问题。代表技术:分布式互斥、分布式共识、分布式选举、分布式选举等。
  • 分布式调度 - 解决分布式系统资源、请求分配调度的问题。代表技术:服务注册和发现、服务路由、负载均衡、流量控制等。
  • 分布式容错 - 解决分布式系统中故障分析、处理的问题,保证系统整体可靠性。代表技术:链路追踪、故障隔离、故障转移等。
  • 分布式部署 - 解决分布式系统部署问题。代表技术:CI/CD、容器化等。

分布式系统的挑战

当程序运行在单机上时,通常会以一种可预测的方式运行:要么正常,要么异常。

一旦程序运行在多台机器上时,面临的场景就会变得复杂而难以预料。在分布式系统中,系统的某些部分可能会出现不可预知的故障,这被称为部分失效(partial failure)。问题的难点就在于部分失效是不确定性的。你甚至不确定请求是否成功了,因为消息通过网络传播的时间也是不确定的!这种不确定性和部分失效的可能性,使得分布式系统难以工作。

扩展阅读:The Eight Fallacies of Distributed Computing - Tech Talk 一文中提出了分布式系统新手常有的 8 种误区。

为什么我们要深刻地认识这 8 个错误?这是因为,我们需要清楚地认识到——在分布式系统中,故障是不可避免的。因此,如果要构建一个可靠的分布式系统,就必须要建立容错机制。很可能大部分组件在大部分时间都正常工作。然而,迟早会有一部分系统出现故障,软件必须以某种方式处理。故障处理必须是软件设计的一部分,并且作为软件的运维,你需要知道在发生故障的情况下,软件可能会表现出怎样的行为。

不可靠的网络

互联网以及大多数数据中心的内部网络(通常是以太网)都是异步网络。当通过网络发送数据包时,数据包可能会丢失或者延迟;同样,回复也可能会丢失或延迟。所以如果没有收到回复,并不能确定消息是否发送成功。传输的过程中,可能有各种各样的问题:

  1. 请求可能已经丢失(可能是被拔掉了网线)。
  2. 请求可能正在某个队列中等待,无法马上发送(可能是网络或接收方已经超负荷)。
  3. 远程接收节点可能已经失效(可能是崩愤或关机)。
  4. 远程节点可能暂时无法响应(例如正在运行长时间的垃圾回收)。
  5. 远程接收节点已经完成了请求处理,但回复却在网络中丢失(例如网络交换机配置错误)。
  6. 远程接收节点已经完成了请求处理,但回复却被延迟处理(例如网络或者发送者的机器过载)。

如果发送请求并没有得到响应,则无法区分(a)请求是否丢失,(b)远程节点是否关闭,或(c)响应是否丢失

在大多数情况下,系统并没有准确判断节点是否发生故障的机制。因此,分布式系统中,一般通过超时检测来判断远程节点是否可用。但是,超时无法区分网络和节点故障,且可变的网络延迟有时会导致节点被误认为发生崩溃。

超时检测的一个关键点是超时大小的设置:

  • 超时时间如果设置过大,意味着等待时间更久,才能判定节点失效(在此期间,用户只能等待或拿到错误信息)。

  • 超时时间如果设置过小,虽然可以更快检测故障,但增加了误判的可能——节点可能实际上是活着的。当一个节点被宣告为失效,其承担的职责要交给到其他节点,这个过程会给其他节点以及网络带来额外负担,特别是如果此时系统已经处于高负荷状态。

对此,可以先设置一个经验值,然后通过实验逐步调整:先在多台机器上,多次测量往返时间,以确定延迟的大概范围;然后结合应用特点,在故障检测与过早超时风险之间选择一个合适的中间值。更好的做法是:持续测量响应时间及其变化(抖动),然后根据最新的响应时间分布来动态调整。

不可靠的时钟

时钟和计时非常重要。有许多应用程序以各种方式依赖于时钟,例如:

  1. 某个请求是否超时了?
  2. 某项服务的 99 %的响应时间是多少?
  3. 在过去的五分钟内,服务平均每秒处理多少个查询?
  4. 用户在我们的网站上浏览花了多段时间?
  5. 这篇文章什么时候发表?
  6. 在什么时间发送提醒邮件?
  7. 这个缓存条目何时过期?
  8. 日志文件中错误消息的时间戳是多少?

在分布式系统中,时间总是件棘手的问题,由于跨节点通信不可能即时完成,消息经由网络从一台机器到另一台机器总是需要花费时间。收到消息的时间应该晚于发送的时间,但是由于网络的不确定延迟,精确测量面临着很多挑战。这些情况使得多节点通信时很难确定事情发生的先后顺序。

为了保证每台机器的时间同步,最常用的机制是 网络时间协议(Network Time Protocol, NTP),它可以根据一组专门的时间服务器来调整本地时间。需要注意的是,即使使用了 NTP 进行时间同步,但是依然会存在一些误差:一方面受限于 NTP 本身的同步精度,此外还受限于网络通信的延迟。

如果想要保证时序,另一种方案是采用逻辑时钟。逻辑时钟(logic clock)是基于递增计数器,对于排序事件来说是更安全的选择。逻辑时钟仅测量事件的相对顺序(无论一个事件发生在另一个事件之前还是之后)。

参考资料

拜占庭将军问题

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

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

问题描述

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

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

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

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

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

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

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

问题分析

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

本文介绍一下口头协议。

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

  • 忠诚的副官遵守同一命令。
  • 若将军是忠诚的,所有忠诚的副官都执行他的命令。
  • 如果叛徒人数为 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 架构

微服务架构

垂直拆分(分库)

水平拆分

多机房部署

服务调用本地化

依赖分级管理

多机房独立部署

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

参考资料

架构实战案例解析

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

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

认识数据系统

单一工具难以满足复杂应用系统的需求,因此整体工作被拆解为一系列能被单个工具高效完成的任务,并通过应用代码将它们缝合起来。比如一个缓存、索引、数据库协作的例子: image.png 一个应用被称为数据密集型的,如果数据是其主要挑战(数据量,数据复杂度、数据变化速度)——与之相对的是计算密集型,即处理器速度是其瓶颈。 软件系统中很重要的三个问题:

  1. 可靠性(Reliability):系统面临各种错误(硬件故障、软件故障、人为错误),仍可正常工作。
  2. 可扩展性(Scalability):有合理的办法应对系统的增长(数据量、流量、复杂性)。
  3. 可维护性(Maintainability):许多不同的人在不同的生命周期,都能高效地在系统上工作。

可靠性

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

可能出错的事情称为错误(fault)或故障,系统可应对错误则称为容错(fault tolerant)或者弹性(resilient)。

故障与失效(failure)不完全一致。故障通常被定义为组件偏离其正常规格,而失效意味着系统作为一个整体,停止对外提供服务。

常见的故障分类:

  • 硬件故障
    • 故障场景:硬盘崩溃、内存故障、停电、断网等。
    • 应对策略:添加冗余硬件以备用;软件容错(如:负载均衡)。
  • 软件故障
    • 故障场景:各种难以预料的 Bug。
    • 应对策略:仔细考虑细节;全面测试;监控、告警;系统/数据隔离机制;自动化部署、回滚机制等。
  • 人为失误
    • 故障场景:操作不当、配置错误等。
    • 应对策略:快速恢复机制;监控、告警等。

可扩展性

可扩展性(Scalability)是用来描述系统应对负载增长能力的术语。

描述负载

负载可以用称为负载参数的若干数字来描述。参数的最佳选择取决于系统的体系结构。它可能是 QPS、数据库中写入的比例、日活用户量、缓存命中率等。

推特发送推文的设计变迁:

推文放在全局推文集合中,查询的时候做 join

image.png

推文插入到每个关注者的时间线中,「扇出」比较大,当有千万粉丝的大 V 发推压力大

image.png

推特从方案一变成了方案二,然后变成了两者结合的方式

描述性能

负责增加将会发生什么:

  1. 负载增加,但系统资源保持不变时,系统性能将受到什么影响?
  2. 负载增加,如果希望性能保持不变时,需要增加多少系统资源?

批处理系统,通常关心吞吐量(throughput);在线系统,通常更关心响应时间(response time)。

度量场景的响应时间,平均响应时间并不是一个合适的指标,因为它无法告诉有多少用户实际经历了多少延迟。最好使用百分位数,比如中位数(P50)、P95、P99、P999 等标识。

image.png

测量客户端的响应时间非常重要(而不是服务端),比如会出现头部阻塞、网络延迟等。

实践中的百分位点,可以用一个滑动的时间窗口(比如 10 分钟)进行统计。可以对列表进行排序,效率低的话,考虑一下正向衰减,t-digest 等近似计算方法。

image.png

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

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

应对负载的方法

  • 垂直扩展:升级硬件
  • 水平扩展:将负载分布到多台小机器上
  • 弹性设计:自动检测负载增加,然后自动添加计算资源
  • 无状态服务可以组成集群进行扩展;有状态服务从单点到分布式,复杂性会大大增加,因此,应该尽量将数据库放在单节点上。

可维护性

三个设计原则:

  • 可运维性:运维更轻松。应对:监控、链路追踪、CI/CD、规范流程等。
  • 简单性:简化复杂度。应对:良好的抽象。
  • 可演化性:易于改变。应对:DDD、TDD、重构、敏捷。

第二章:数据模型与查询语言

关系模型与文档模型

关系模型 - 数据被组织成关系(SQL 中称作),其中每个关系是元组(SQL 中称作) 的无序集合。

NoSql - 不仅是 SQL(Not Only SQL)

相比于关系型数据库,为什么用 NoSql?

  • 需要更好的扩展性,以应对非常大的数据集或高并发。
  • 关系模型不能很好地支持一些特殊的查询。
  • 关系模型有很多限制,不够灵活。

当前以及未来很长一段时间,关系型数据库和 NoSql 并存的混合持久化是一种常态。

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

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

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

对象关系不匹配

使用面向对象语言,需要一个转换层,才能转成 SQL 数据模型。模型之间的脱离有时被称为阻抗失谐。

Hibernate 这样的 对象关系映射(ORM) 框架则减少这个转换层所需的样板代码量,但是它们不能完全隐藏这两个模型之间的差异。

对于一份简历而言,关系型模型描述一对多的关系需要多张表。

image.png 对于简历这样的数据结构,主要是一个自包含的文档,用 JSON 表示非常合适。JSON 相比于多表模式,有更好的局部性,可以一次查询出一个用户的所有信息。JSON 其实是树形层级结构。image.png

多对一和多对多的关系

使用 ID 的好处是,因为它对人类没有任何直接意义,所以永远不需要直接改变:即使 ID 标识的信息发生了变化,它也可以保持不变。

文档模型不适合表达多对一的关系。对于关系数据库,由于支持联结操作,可以更方便地通过 ID 来引用其他表的行。而在文档数据库中,一对多的树状结构不需要联结,即使支持联结通常也比较弱。

如果数据库本身不支持联结,则必须通过对数据库进行多次查询来模拟联结。

考虑以下可能对简历进行的修改或补充:

  • 组织和学校作为实体:组织、学校有各自的主页。
  • 推荐:用户可以推荐其他用户在自己的简历上。

image.png

文档数据库是否在重演历史?

20 世纪 70 年代,最受欢迎的是层次模型(hierarchical model),它与文档数据库使用的 JSON 模型有很多相似之处。它将所有数据表示为嵌套在记录中的记录树。层次模型能很好地支持一对多的关系,但是很难支持多对多的关系,而且不支持联结。

为解决层次模型的局限性而提出的方案:

  • 关系模型(relational model) - 后来,演变成了 SQL,并被广泛接受
  • 网络模型(network model) - 最初很受关注,但最终被淡忘

image.png

网络模型

每个记录可能有多个父节点。

网络模型中,记录之间的链接不是外键,而更像编程语言中的指针(会存储在磁盘上)。访问记录的唯一方法是选择一条始于根记录的路径,并沿着相关链接一次访问,这条链接链条也被称为访问路径(access path)

最简单的情况下,访问路径类似遍历链表:从链表头开始,每次查看一条记录,直到找到所需的记录。但在多对多关系的情况中,存在多条不同的路径可以通向相同的记录,网络模型的程序员必须跟踪这些不同的访问路径。

缺点:查询和更新数据库非常麻烦。

关系模型

关系模型定义了所有数据的格式:关系(表) 只是 元组(行) 的集合,仅此而已。

在关系数据库中,查询优化器自动决定以何种顺序执行查询,以及使用哪些索引。

文档数据库的比较

文档数据库是某种方式的层次模型:即在其负记录中保存了嵌套记录,而不是存储在单独的表中。

但是,在表示多对一和多对多的关系时,关系数据库和文档数据库并没有根本的不同:在这两种情况下,相关项目都由唯一的标识符引用,该标识符在关系模型中被称为外键,在文档模型中被称为文档引用。标识符可以查询时通过联结操作或相关后续查询来解析。

关系数据库与文档数据库现状

支持文档数据模型的主要论据是模式灵活性,由于局部性而带来较好的性能。关系模型则强在联结操作、多对一和多对多关系更简洁的表达上。

哪种数据模型的应用代码更简单

文档模型:

  • 优点:
    • 如果应用程序中的数据具有类似文档的结构(即一对多关系树,通常一次性加载整个树),那么使用文档模型更为合适。而关系模型则倾向于数据分解,把文档结构分解为多个表。
  • 缺点:
    • 不能直接引用文档中的嵌套的项目,而是需要说“用户 251 的位置列表中的第二项”(很像分层模型中的访问路径)。但是,只要文件嵌套不太深,这通常不是问题。
    • 文档数据库对联结的支持不足。这是否是问题取决于应用,如果应用程序使用多对多关系,那么文档模型就没不合适了。

对于高度关联的数据,文档模型不太适合,关系模型更适合。

文档模型中的模式灵活性

文档模型是「读时模式」

  • 文档数据库有时称为无模式(schemaless),但这具有误导性,因为读取数据的代码通常假定某种结构——即存在隐式模式,但不由数据库强制执行。
  • 一个更精确的术语是读时模式(schema-on-read)(数据的结构是隐含的,只有在数据被读取时才被解释),相应的是写时模式(schema-on-write)(传统的关系数据库方法中,模式明确,且数据库确保数据写入时都必须遵循)。
  • 读时模式类似于编程语言中的动态(运行时)类型检查,而写时模式类似于静态(编译时)类型检查。

模式变更

  • 读时模式变更字段很容易,只用改应用代码
  • 写时模式变更字段速度很慢,而且要求停运。它的这种坏名誉并不是完全应得的:大多数关系数据库系统可在几毫秒内执行 ALTER TABLE 语句。MySQL 是一个值得注意的例外,它执行 ALTER TABLE 时会复制整个表,这可能意味着在更改一个大型表时会花费几分钟甚至几个小时的停机时间,尽管存在各种工具来解决这个限制。
查询的数据局部性

文档通常存储为编码为 JSON、XML 或其二进制变体(如 MongoDB 的 BSON)的连续字符串。

读文档:

  • 如果应用需要频繁访问整个文档,则存储局部性具有性能优势。
  • 局部性优势仅适用于需要同时访问文档大部分内容的场景。

写文档:

  • 更新文档时,通常需要重写整个文档。
  • 通常建议文档应该尽量小且避免写入时增加文档大小。
文档数据库与关系数据库的融合
  • MySQL 等逐步增加了对 JSON 和 XML 的支持
  • 融合关系模型与文档模型是未来数据库发展的一条很好的途径。

数据查询语言

  • 关系模型包含了一种查询数据的新方法:SQL 是一种 声明式 查询语言,而 IMS 和 CODASYL 使用 命令式 代码来查询数据库。

  • 命令式语言告诉计算机以特定顺序执行某些操作,比如常见的编程语言。

  • 声明式查询语言只需指定所需的数据模式,结果需要满足哪些条件,以及如何转换数据(例如,排序,分组和集合) ,而不需指明如何实现这一目标

Web 上的声明式查询(略)

MapReduce 查询

MapReduce 是一种编程模型,用于在许多机器上批量处理海量数据。一些 NoSQL 支持有限的 MapReduce 方式在大量文档上执行只读查询。

图数据模型(略)

本章小结

历史上,数据最初被表示为一棵大树(层次模型),但是这不利于表示多对多的关系,所以发明了关系模型来解决这个问题。 最近,开发人员发现一些应用程序也不适合采用关系模型。新的非关系型“NoSQL”数据存储在两个主要方向上存在分歧:

  • 文档数据库的应用场景是:数据来自于自包含文档,且文档之间的关联很少。
  • 图数据库则的应用场景是:所有数据都可能会相互关联。

文档模型、关系模型和图模型,都应用广泛。不同模型之间可以相互模拟,但是处理起来比较笨拙。

文档数据库和图数据库有一个共同点,那就是它们通常不会对存储的数据强加某个模式,这样比较灵活。

第三章:存储与检索

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

数据库核心:数据结构

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

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

索引类型:

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

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

事务处理与分析处理

列式存储

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

第四章:数据编码与演化

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

数据编码格式

数据流模式

向前和向后的兼容对于可演化性来说非常重要。

基于数据库的数据流

在不同的时间写入不同的值

数据库通常支持在不同的时间写入不同的值。

在集群中部署新版本是一个逐一的过程,必然存在这样的时间段:集群中部分是新机器,部分是老机器。

当旧版本的应用视图更新新版本的应用所写入的数据时,可能会丢失数据。

image.png

归档数据

生成数据库快照时,数据转储通常使用最新的模式进行编码。

基于服务的数据流:REST 和 RPC

  • 最常见的网络通信方式:C/S 架构(客户端+服务端)。
  • Web 服务:收、发 GET 和 POST 请求。
  • 将大型应用分而治之:微服务架构。
  • 微服务架构的一个关键设计目标:服务可以独立部署和演化。
Web 服务
  • 当 HTTP 被用作与服务通信的底层协议时,它被称为 Web 服务
  • 有两种流行的 Web 服务方法:REST 和 SOAP。

REST 不是一种协议,而是一个基于 HTTP 原则的设计理念。它强调简单的数据格式,使用 URL 来标识资源,并使用 HTTP 功能进行缓存控制,身份验证和内容类型协商。与 SOAP 相比,REST 已经越来越受欢迎,至少在跨组织服务集成的背景下,并经常与微服务相关。根据 REST 原则设计的 API 称为 RESTful。

SOAP 是一种基于 XML 的协议,用于发送网络 API 请求。虽然,它最常用于 HTTP,但其目的是独立于 HTTP,并避免使用大多数 HTTP 功能。SOAP Web 服务的 API 使用 WSDL 语言来描述。 WSDL 支持代码生成,客户端可以使用本地类和方法调用(编码为 XML 消息并由框架再次解码)访问远程服务。尽管 SOAP 及其各种扩展表面上是标准化的,但是不同厂商的实现之间的互操作性往往会造成问题。

远程过程调用(RPC)的问题

RPC 模型试图向远程网络服务发出请求,看起来与在同一进程中调用编程语言中的函数或方法相同(这种抽象称为位置透明)。

RPC 的缺陷:

  • 本地函数调用是可预测的,并且成功或失败仅取决于控制的参数。而网络请求是不可预知的。
  • 本地函数调用要么返回结果,要么抛出异常,或者永远不返回(因为进入无限循环或进程崩溃)。网络请求有另一个可能的结果:由于超时,它可能会没有返回结果。这种情况下,无法得知发生了什么。
  • 如果重试失败的网络请求,可能会发生请求实际上已经完成,只有响应丢失的情况。在这种情况下,重试将导致该操作被执行多次,除非在协议中建立重复数据消除( 幂等(idempotence))机制。本地函数调用没有这个问题。
  • 每次调用本地功能时,通常需要大致相同的时间来执行。网络请求慢得多,不可预知。
  • 调用本地函数时,可以高效地将引用(指针)传递给本地内存中的对象。当发出网络请求时,所有这些参数都需要被编码成可以通过网络发送的字节序列。如果参数是像数字或字符串这样的基本类型倒是没关系,但是对于较大的对象很快就会变成问题。
  • 客户端和服务端可以用不同的编程语言实现。所以,RPC 框架必须将数据类型从一种语言转换成另一种语言。

RPC 比 REST 性能好。但是,REST 更加方便,不限定特定的语言,有更好的通用性。因此,REST 是公共 API 的主流;RPC 框架则侧重于同一组织内多个服务间的请求,且通常在同一数据中心。

基于消息传递的数据流

消息代理

通常,消息代理的使用方式如下:

生产者向指定的队列或主题发消息;消息代理确保消息被传递给队列或主题的一个或多个消费者或订阅者。同一主题上,可以有多个生产者和多个消费者。

分布式 Actor 框架

Actor 模型是用于单个进程中并发的编程模型。每个 Actor 通常代表一个客户端或实体,它可能具有某些本地状态,并且它通过发送和接受异步消息与其他 Actor 通信。

分布式的 Actor 框架实质上时将消息代理和 Actor 编程模型集成到单个框架中。

三种流行的分布式 Actor 框架:

  • Akka 使用 Java 的内置序列化,它不提供向前或向后兼容性。但是,可以用类似 Protocol Buffer 替代;
  • Orleans 不支持滚动升级部署的自定义数据编码格式;
  • Erlang OTP,很难对记录模式进行更改。

小结

许多服务需要支持滚动升级:向前、向后兼容性。

我们讨论了几种数据编码格式及其兼容性属性:

  • 编程语言特定的编码仅限于单一编程语言,往往无法提供前向和后向兼容性。
  • JSON,XML 和 CSV 等文本格式非常普遍,其兼容性取决于您如何使用它们。它们有可选的模式语言,这有时是有用的,有时却是一个障碍。这些格式对某些数据类型的支持有些模糊,必须小心数字和二进制字符串等问题。
  • 像 Thrift,Protocol Buffers 和 Avro 这样的二进制模式驱动格式,支持使用清晰定义的前向和后向兼容性语义进行紧凑,高效的编码。这些模式对于静态类型语言中的文档和非常有用。但是,他们有一个缺点,就是在数据可读之前需要对数据进行解码。

我们还讨论了数据流的几种模式,说明了数据编码重要性的不同场景:

  • 数据库,写入数据库的进程对数据进行编码,并从数据库读取进程对其进行解码。
  • RPC 和 REST API,客户端对请求进行编码,服务器对请求进行解码并对响应进行编码,客户端最终对响应进行解码。
  • 异步消息传递(使用消息代理或 Actor),节点之间通过互发消息进行通信,消息由发送者编码并由接收者解码。

结论:前向兼容性和滚动升级在某种程度上是可以实现的。

参考资料