admin管理员组

文章数量:1646244

数据密集型应用通常也是基于标准模块构建而成,每个模块负责单 的常用功能。例 如,许多应用系统都包含以下模块

  • 数据库:用以存储数据,这样之后应用可以再次面问。
  • 高速缓存 缓存那些复杂或操作代价昂贵的结果,以加快下一次访问。
  • 索引 用户可以按关键字搜索数据井支持各种过滤
  • 流式处理:持续发送消息至另 个进程,处理采用异步方式。
  • 批处理 定期处理大量的累积数据。
  • 可靠性:当出现意外情况如硬件、软件故障、人为失误等,系统应可以继续正常运转
  • 可扩展性:随着规模 增长 ,例如数据 、流量或复杂性,系统应以合理的方式来匹配这种增长
  • 可维护性:许多新的人员参与到系统开发和运维, 以维护现有功能或适配 新场景等,系统都应高效运转

数据编码:JSON,XML和CSV等文本格式,或者Thrift,Protocal Buffers 和 Avro二进制处理模式。

数据流模式:

  • 数据库,其中写入进程对数据进行编码,读取数据库进程对数据进行解码
  • RPC和Rest API, 其中客户端对请求进行编码,服务器对请求进行解码,并对相应进行编码,客户端最终对响应进行解码
  • 异步消息传递(Actor或者消息中间件),节点之间通信通过互相发送消息进行通信

1. 主节点与从节点

1. 同步复制&异步复制

2. 配置新的从节点

  • 如何确保新的从节点和主节点保持数据一致
    • 简单的文件复制,(客户端仍在不停地写,数据处于变化之中),导致不同节点上呈现不同时间点数据
    • 锁定数据库:违反高可用设计原则
  1. 在某个时间点对主节点的数据副本产生一个 致性快照,这样避免长时间锁定整 个数据库。目前大多数数据库都支持此功能,快照也是系统备份所必需的。而在 某些情况下,可能需要第 方工具, MySQL innobackupex
  2. 将此快照拷贝到新的从节点
  3. 从节点连接到主节点并请求快照点之后所发 的数据更改日志。因为在第一步 建快照时,快照与系统复制日志的某个确定位置相 联,这个位置信息在不同的 系统有不同的称 乎,如PostgreS QL 其称为“ log sequence number” (日志序列 号),而MySQL将其称为“binlog coordinates
  4. 获得日志之后,从节点来应用这些快照点之后所有数据变更,这个过程称之为追 赶。接下来,它可以继 处理主节点上新的数据变化。井重复步骤 ~步骤

3. 节点失效

.1. 从节点失效-追赶模式

从节点保存副本数据变更复制日志,可以指导发生故障之前所处理的最后一笔事务,然后连接到主节点,并请求自上次事务后中断期间所有的数据变更应用到本地。

.2. 主节点失效-节点切换

主节点失败时需要提升某个从节点为新的主节点,同时需要通知客户端新的主节点,其他从节点接受来自新的主节点数据变更。

自动切换主节点的步骤通常如下

  1. 确认主节点失效。大部分系统采用基于超时的机制主从节点直接发送心跳消息,主节点在某个时间内都没有响应,则认为主节点已经失效
  2. 选举新的主节点。通过选举的方式(超过半数以上的从节点达成共识)来选举新的主节点,新的主节点是与旧的主节点数据差异最小的一个,最小化数据丢失的风险。(共识问题)
  3. 重新配置使新的主节点上线。(请求路由)

除了以上步骤之外,还有以下问题需要考虑:

  1. 如果使用异步复制机制,而且在失效之前,新的主节点并没有收到旧的主节点的所有数据,那么在旧的主节点重新上线之后,旧的主节点上未完成复制的数据将被丢弃(违背了数据持久化)
  2. 可能会出现集群同时存在两个主节点的情况,也就是所谓的脑裂(split brain)现象,此时两个主节点都认为自己是主节点并且都能接收客户端的写数据请求,会导致数据丢失或者破坏。
  3. 如何设置合理的超时时间来判断主节点失效?如果太大意味着总体恢复时间长,如果太小意味着某些情况下可能主节点并未失效但是被误判为失效了,比如网络峰值导致延迟高等原因,这样会导致很多不必要的主节点切换。

上述的问题,包括节点失效网络不可靠副本一致性持久性可用性延迟之间的各种细微的权衡,正是分布式系统核心的基本问题。

4. 复制日志实现

.1. 基于语句的复制

主节点记录所执行的每个写请求并将该语句做为日志发送给从节点。但是有些场景并不适合这么做,比如:

  • 调用任何非确定函数的语句,比如NOW () 获得当前时间,RAND () 返回一个随机数。
  • 语句中使用了自增列,或者依赖于当前数据库的数据
  • 有副作用的语句(触发器,存储过程,用户自定义函数等),在每个副本上面执行的效果不一样。
.2. 基于预写日志 (WAL)

对数据库的操作写入日志,传送到从节点上然后执行,得到与主节点相同的数据副本。

  • 对于日志结构的存储引擎,日志是主要存储方式,日志段在后台压缩并支持垃圾回收
  • 采用覆盖写磁盘Btree结构,每次修改会预先写入日志。
  • 一个WAL包含了哪些磁盘块的哪些自己发生改变。
.3. 基于行的逻辑日志复制

所谓的逻辑日志,就是复制与存储引擎采用不同的日志格式,这样复制与存储逻辑剥离,这种日志称为逻辑日志,与物理存储引擎的数据区分开。由于逻辑日志与存储引擎逻辑上解耦,因此可以更好的向后兼容,也更好的能被外部程序解析。(如果不是行存储的怎么办)

对于关系型数据库,其逻辑日志是一系列用来描述数据表行级别的写请求:

  • 插入行:日志包括所有相关列的新值
  • 删除行:日志中保证要有足够的信息来唯一标识待删除的行,通常是主键
  • 更新行:日志中保证要有足够的信息来唯一标识待更新的行,同时也有所有列的新值
.4. 基于触发器的复制

指向复制数据的一部分,或者想从一种数据库复制到另一种数据库,或者需要定制,管理冲突解决逻辑,将复制控制交给应用程序层。

2. 复制滞后

正常情 况下,主节点和从节点上完成写操作之间的时间延迟(复制滞后)可能不足 秒,这 样的滞后,在实践中通常不会导致太大影响。但是,如果系统已接近设计上限,或者 网络存在问题, 滞后可能轻松增 秒甚至 分钟不等。暂时的不一致性,最终的一致性。

.1. 读自己的写(reading your own writes)

用户在写入数据不久就马上查看数据,而新数据并未到达从节点,这样在用户看来可能读到了旧的数据。这样情况需要 “写后读一致性(read-after-write consistency)”,该机制保证每次用户读到的都是自己最近的更新数据,但是对其他用户则没有任何保证。

在上图中,用户 1234 首先向主节点写入数据,SQL 执行成功之后返回,而此时用户再次向从节点 2 发起读刚才写入数据的请求,但是却读到了旧的数据。

有以下方案实现写后读一致性。

  • 如果用户访问可能会被修改的内容,从主节点读取。比如社交网络的本用户首页信息只会被本人修改,访问用户自己的首页信息通过主节点,而访问其他用户的首页信息则走的从节点。
  • 如果应用大部分内容都可能被所有用户修改,则上述方法不太适用。此时需要其他机制来判断哪些请求需要走主节点,比如更新后一分钟之内的请求都走的主节点。
  • 客户端可以记住自己最近更新数据的时间戳,在请求数据时带上时间戳,如果副本上没有至少包含该时间戳的数据则转发给其他副本处理,直到能处理为止。但是在这里,“时间戳” 可以是逻辑时钟(比如用来指示写入数据的日志序列号)或者实际系统时钟(而使用系统时间又将时间同步变成了一个关键点)。
  • 如果副本分布在多数据中心,必须将请求路由到主节点所在的数据中心

如果同 用户可能会从多个设备访问数据,例如 个桌面We 浏览器和 个移动端的 应用,情 会变得更加复杂 此时 ,要提供跨设备的写后读一致性, 即如果用户在某 个设备上输入了 些信息然后在另 台设备上查看,也应该看到刚刚所输入的内容.

  • 如果同 用户可能会从多个设备访问数据,例如 个桌面We 浏览器和 个移动端的 应用,情 会变得更加复杂 此时 ,要提供跨设备的写后读一致性, 即如果用户在某 个设备上输入了 些信息然后在另 台设备上查看,也应该看到刚刚所输入的内容
  • 如果副本分布在多数据中心, 无法保证来自不同设备的连接经过路由之后都到达同一个数据中心

.2. 单调读(monotonic reads)

单调读一致性保证不会发生多次读同一条数据出现回滚(moving backward)的现象。这个是比强一致性弱,但是比最终一致性强的保证。

在上图中,用户 2345 发起了两次读请求,第一次向从节点 1 发起的请求拿到了最新的数据,但是第二次向从节点 2 发起的请求得到了旧的数据,这在用户看来,数据发生了 “回滚”。

单调读一致性可以确保不会发生这种异常。当读取数据时,单调读保证:如果某个用户进行多次读取,则绝对不会看到数据回滚现象,即在读取到新值之后又发生读取到旧值的情况。

实现单调读一致性的一种方式每个用户的每次读取都从固定的同一副本上进行读取

.3. 前缀一致读(consistent prefix reads)

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

例如,正常情况下,是如下的对话:

  • poons 先生:cake 小姐,您能看见多远的未来?
  • cacke 小姐:通常约 10 秒,poons 先生。

但是在上图中,从观察者角度,数据的先后顺序发生了混淆,导致了逻辑上的混乱。这种问题是分区情况下出现的特殊问题,在分布式数据库中,不同的分区独立运行,因此不存在全局写入顺序,这就导致用户从数据库中读取数据时,可能看到数据库某部分的旧值和一部分的新值

实现前缀一致性的一种方案是确保任何具有因果顺序关系的写入都交给一个分区来完成,但是该方案真实实现起来效率不高。

3. 多主节点复制

.1. 场景-多数据中心

为了容忍整个数据中心级别故障或更接近用户,可以把数据库的副本横跨多个数据中心在每个数据中心内,采用常规的主从复制方案;而在数据中心之间,由各个数据中心的主节点来负责同其他数据中心的主节点进行数据的交换、更新

  • 性能:每个写操作可以在本地数据中心就近快速响应采用异步复制方式将变化同步到其他数据中心
  • 容忍数据中心失败:单个数据中心失败,不影响其他数据中心的继续运行
  • 容忍网络问题:主从复制模型中写操作是同步操作,对数据中心之间的网络性能和稳定性等要求更高。多主节点模型采用异步复制,可以更好的容忍这类问题。

不同的数据中心可能会同 修改相同的数据,因而必须解决潜在的写冲突(如图5-6 中的“冲突解决”)

.2. 场景-离线客户端操作

在离线状态下进行的任何更改,会在下次设备上线时,与服务器以及其他设备 同步。

每个设备都有 个充当主节点的本地数据库(用来接受写请求),然后 在所有设备之间采用异步方式同步这些多主节点上的副本,同步滞后可能是几小时或 者数天,具体时间取决于设备何时可以再次联网。

.3. 场景-协作编程

当一个用户编辑文档时 ,所做的更改会立即应用到本地副本( We 览器或客户端应 用程序),然后异步复制到服务器以及编辑同 文档的其他用户。这样每个用户就是一个独立的数据中心了。

.4. 处理写冲突

冲突解决通常用于单个行或文档,而不是整个事务,如果一个原子事务包含了多个不同写请求,每个写请求仍然是分开考虑的解决冲突。

1. 同步与异步冲突检测

如果是主从复制数据库,第二个写请求会被阻塞到第一个写请求完成。而在多主从复制模型下,两个写请求都是成功的,并且只有在之后才能检测到写冲突,而那时候要用户来解决冲突已经为时已晚了。

如果要多主从复制模型来做到同步检测冲突,又失去了多主节点的优势:允许每个主节点接受写请求。

因此如果确实想要做到同步检测写冲突,应该考虑使用单主节点的模型而不是多主从节点模型。

2. 避免冲突

如果应用层可以保证对特定记录的写请求总是通过一个主节点,这样就不会发生写冲突。

但是,在数据中心发生故障,不得不路由请求到另外的数据中心,或者用户漫游到了另一个位置,更靠近另一个数据中心等场景下,冲突避免不再有效。

3. 收敛于一致状态

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

  • 给每个写入分配唯一的 ID,如时间戳、足够长的随机数、UUID 等,规定只有高 ID 的写入做为胜利者。如果是基于时间戳的对比,这种技术被称为后写入者获胜(last write win),但是很容易造成数据丢失
  • 每个副本分配一个唯一的 ID,并制定规则比如最高 ID 的副本写入成功,这种方式也会导致数据丢失。
  • 某种方式将这些值合并在一起
  • 使用预定义的格式将这些冲突的值返回给应用层,由应用层来解决
4. 自定义冲突解决逻辑

解决冲突最合适的方式还是依靠应用层,可以在写入或者读取时执行。

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

为防止无限循环,每个节点需要赋予 个唯 标识符,在复 日志中的每个写请求都标记了已通过的节点标识符(43 ]。如果某个节点 收到了包含自身标识符的数据更改,表明该请求已经被处理过,因 会忽略 变更请 求,避免重复转发。

全链接拓扑也存在一些自身的问题。主要是存在某些网络链路比其他链 路更快的情况(例如由于不同网络拥塞),从而导致复制日志之间的覆盖

客户端 主节点 表中首先插入 行,然后客户端 在主节点 上对该 行记录进行更新。而在主节点 上,由于网络原因可能出现意外的写日志复制顺序, 例如它先接收到了主节点 的更新日志(从主节点 的角度来看,这是对数据库中不存 在行的更新操作),之后才接收到主节点 的插入日志(按道理应该在更新日志之前 到达)。

  • 在每笔写日志里简单地添加时间戳还不够,主要因为无能确保时钟 完全同步
  • 版本向量技术,检测并发写入

4. 无主节点复制

对于某些无主节点系统实现,客户端直接将其写请求发送到多副本,而在其他 些实 现中,由一个协调者节点代表客户端进行写人,但与主节点的数据库不同,协调者井 不负责写入顺序的维护

.1. 节点失效时写入数据库

当一个客户端从数据库中读取数据肘,它不是向 个副本发送请 求,而是井行地发送到多个副本。客户端可能会得到不同节点的不同响应,包括某些 节点的新值和某些节点的旧值。可以采用版本号技术确定哪个值更新(参见本章后面 的“检测并发写入”)。

针对副本三因为掉线到时没有跟新最新的数据,更新策略:

  • 读修复:客户端读取多个副本的时候,可以检测出过期的返回值,然后将新值写入该副本中
  • 反熵过程:一些数据存储后,有后台进程不断地查找副本之间的数据差异,将任何缺少的数据从一个副本复制到另一个副本中。(不保证以特定的顺序写入,并且会引入明显地同步滞后)

.2. 读写 quorum

如果有n 个副本,写人需要 w个节点确认,读取必须至少 查询 r节点, 则只要 w+r>n ,读取的节点中一定会包含最新值。一般设置w=r=(n+1)/2; (仲裁读、写)

.3. quorum 一致性的局限性

  • 如果采用了 sloppy quorum (参阅本章后面的“宽松的quorum与数据回传”), 写操作 节点和 读取的 节点可能完全不同,因此无法保证读写请求一定存在重叠 的节点 。
  • 如果两个写操作同时发生,则无法明确先后顺序。这种情况下,唯一安全的解决方案是合并并发写入(参见本章前面的“处理写冲突”)。如果根据时间戳(最后写入获胜)挑选胜者,则由于时钟偏差问题!35],某些写入可能会被错误地抛弃。
  • 如果写操作与读操作同时发生,写操作可能仅在一部分副本上完成。此时,读取时返回旧值还是新值存在不确定性
  • 如果某些副本上已经写入成功,而其他一些副本发生写入失败(例如磁盘已满),且总的成功副本数少于w,那些已成功的副本上不会做回滚。这意味着尽管这样的写操作被视为失败,后续的读操作仍可能返回新值[47].
  • 如果具有新值的节点后来发生失效,但恢复数据来自某个旧值,则总的新值副本数会低于w,这就打破了之前的判定条件。
  • 即使一切工作正常,也会出现一些边界情况,如第9章所介绍的“可线性化与quorum”。
1. 监控旧值

从运维角度来看,监视数据库是否返回最新结果非常重要。即使应用程序可以容忍读取旧值,也需要仔细了解复制的当前运行状态。如果已经出现了明显的滞后,它就是个重要的信号提醒我们需要采取必要措施来排查原因(例如网络问题或节点超负荷)。

2. 宽松地quorum与数据回传
  • 如果无法达到w或r所要求quorum,将错误明确地返回给客户端?
  • 或者,我们是否应该接受该写请求,只是将它们暂时写入一些可访问的节点中?注意,这些节点并不在n个节点集合中
3. 多数据中心操作

Cassandra和Voldemort在其默认配置的无主节点模型中都支持跨数据中心操作:副本的数量n是包含所有数据中心的节点总数。配置时,可以指定每个数据中心各有多少副本每个客户端的写入都会发送到所有副本,但客户端通常只会等待来自本地数据中心内的quorum节点数的确认,这样避免了高延迟和跨数据中心可能的网络异常。尽管可以灵活配置,但对远程数据中心的写入由于延迟很高,通常都被配置为异步方式150.51]

.4. 检测并发写

一个核心问题是,由于网络延迟不稳定或者局部失效,请求在不同的节点上可能会呈现不同的顺序。

1. 最后写入获胜(丢弃并发写)(LWW)
  • 为每个写请求附加一个时间戳,然后选择最新地及时间戳最大的,丢弃较早时间戳地写入。(丢失数据是否可以接受)
  • 只写入一次然后写入值视为不可变,避免对同一个主键的并发覆盖写。(采用UUID作为主键,每个写操作都针对不同的,系统唯一地主键)todo 这句话没太理解
2. Happens-before 关系和并发
  • 如果俩操作都不在另一个操作之前,则是并发,否则有因果关系 happens-before

  • 服务器为每个主键维护一个版本号,每当主键新值写入时递增版本号,并将新版本号与写入的值一起保存。
  • 当客户端读取主键时,服务器将返回所有(未被覆盖的)当前值以及最新的版本号。且要求写之前,客户必须先发送读请求。
  • 客户端写主键,写请求必须包含之前读到的版本号、读到的值和新值合并后的集合。写请求的响应可以像读操作一样,会返回所有当前值,这样就可以像购物车例子那样一步步链接起多个写入的值。
  • 当服务器收到带有特定版本号的写入时,覆盖该版本号或更低版本的所有值(因为知道这些值已经被合并到新传入的值集合中),但必须保存更高版本号的所有值(因为这些值与当前的写操作属于并发)。

当写请求包含了前一次读取的版本号时,意味着修改的是基于以前的状态。如果一个写请求没有包含版本号,它将与所有其他写入同时进行,不会覆盖任何已有值,其传入的值将包含在后续读请求的返回值列表当中。(通过版本号,可以实现数据不丢弃)

3. 合并同时写入地值
  • 如果合并了两个客户端的值,且其中有一个商品被某客户端 除掉,被删除的项目会再次出现在合并的终值中(项目在删除时不能 简单地从数据 中删除 ,系统必须保留一个对应的版本号以恰当的标记该项目需要在合井时被剔除。这种删除标记被称为墓碑)(CRDT 数据结构todo)
4. 版本矢量
  • 当多个副本同时接受写入时,我们需要为每个副本和每个主键均定义一个版本号。每个副本在处理写入时增加白己的版本号,并且跟踪从其他副本看到的版本号。通过这些信息来指示要覆盖哪些值、该保留哪些并发值。
  • 所有副本的版本号集合称为版本矢量。当读取数据时,数据库副本会返回版本矢量给客户端,而在随后写入时需要将版本信息包含在请求当中一起发送到数据库。Riak将版本矢量编码为一个称之为因果上下文的字符串。版本矢量技术使数据库可以区分究竟应该覆盖写还是保留并发值。
  • 就像单副本的例子一样,应用程序仍然需要执行合并操作。版本矢量可以保证从某一个副本读取值然后写入到另一个副本,而这些值可能会导致在其他副本上衍生出来新的“兄弟”值,但至少不会发生数据丢失且可以正确合并所有并发值。

Resource

  • 《数据密集型应用系统设计》

本文标签: 分布式数据