<![CDATA[1Feng's Blog]]> 2021-04-14T20:38:00+08:00 http://1feng.github.io/ Octopress <![CDATA[hello world]]> 2021-04-14T20:37:00+08:00 http://1feng.github.io/2021/04/14/hello-world <![CDATA[Read-Only 的 linearizability]]> 2017-06-16T23:59:00+08:00 http://1feng.github.io/2017/06/16/smarter

《Paxos Replicated State Machines as the Basis of a High-Performance Data Store》 介绍了使用了paxos算法进行副本同步,这里仅总结如何保证read-only操作的linearizability

How

  1. 收到read-only请求后,记录下一个slot number
  2. slot number = max(已经commit的最大的operation number VS 当前节点成为leader后re-proposed最大的operation number)
  3. 向所有replicas发送消息,检查是否有新leader出现(即检查当前节点是否扔是合法leader)
  4. 如果加上自身有总数过半的节点仍然认为当前节点是leader则继续,否则丢弃请求
  5. 将请求连同1中记录的slot number转发到任意replica(最好是3中回复确认的的replica),称之为replica A
  6. replica A等待slot number被执行,之后检测是否有新的paxos configuration被选择,如果有则丢弃请求,否则执行read操作返回结果

Why

最简单的保证linearizability的read-only的方法是将read-only操作当做写操作一样走一遍paxos流程,但是这样读的性能太低了,并且会导致leader压力巨大

论文中提出的方法省去了走paxos流程的磁盘IO,仅一次广播检测确认leader角色,并将真正的读操作转移到了replica上

那么如何证明呢?

  1. read-only linearizability需要保证的是在这个请求到达之前已经成功提交的写入都应该被本次读取看到
  2. 我们将read-only request 到来之前已经成功提交的最后一条写入的operation number为 N,则有以下三种情况:
  3. N 是前一个leader提交的
  4. N 是当前节点成为leader后提交的
  5. 当前节点早已经不是leader, N其实是后续leader提交的
  6. 我们只需保证slot number >= N即可保证linearizability
  7. N是前一个leader提交的,当前节点成为leader后re-proposed最大的operation number 一定大于等于N
  8. N是当前leader提交的,那么一定有slot number >= N
  9. 该情况请求会被丢弃,slot number不需要保证大于等于N

Extension

TIDB中在使用raft做数据同步的情况下,也使用了一个类似的方法来保证read-only的linearizability:

当 leader 要处理一个读请求的时候: 1. 将当前自己的 commit index 记录到一个 local 变量 ReadIndex 里面。 2. 向其他节点发起一次 heartbeat,如果大多数节点返回了对应的 heartbeat response,那么 leader 就能够确定现在自己仍然是 leader。 3. Leader 等待自己的状态机执行,直到 apply index 超过了 ReadIndex,这样就能够安全的提供 linearizable read 了。 4. Leader 执行 read 请求,将结果返回给 client。

其中:

实现 ReadIndex 的时候有一个 corner case,在 etcd 和 TiKV 最初实现的时候,我们都没有注意到。也就是 leader 刚通过选举成为 leader 的时候,这时候的 commit index 并不能够保证是当前整个系统最新的 commit index,所以 Raft 要求当 leader 选举成功之后,首先提交一个 no-op 的 entry,保证 leader 的 commit index 成为最新的。

与本文中N是前一个leader提交的,当前节点成为leader后re-proposed最大的operation number 一定大于等于N是类似的(raft毕竟是paxos的变种)

另外一种方式就是TIDB根据raft论文实现的lease的方式:

在 Raft 论文里面,提到了一种通过 clock + heartbeat 的 lease read 优化方法。也就是 leader 发送 heartbeat 的时候,会首先记录一个时间点 start,当系统大部分节点都回复了 heartbeat response,那么我们就可以认为 leader 的 lease 有效期可以到 start + election timeout / clock drift bound 这个时间点。

为什么能够这么认为呢?主要是在于 Raft 的选举机制,因为 follower 会在至少 election timeout 的时间之后,才会重新发生选举,所以下一个 leader 选出来的时间一定可以保证大于 start + election timeout / clock drift bound。

Referrence

  1. TiKV 源码解析系列 - Lease Read
]]>
<![CDATA[线性一致性]]> 2017-06-15T23:59:00+08:00 http://1feng.github.io/2017/06/15/linearizability Introduce

所谓的linearizability其目的在于描述系统的数据,对外看起来就像只有一份,所有针对这部分数据的操作都是原子(Concurrency-atomic)的;在分布式系统领域来讲和CAP-consistent是等价的;在多核并发编程时由于存在CPU-Cache一致性问题,linearizability的概念同样适用。

What

通用的定义(分布式系统 and 多核系统)

every read returns the latest value written into the shared variable preceding that read operation, then the shared object is linearizable

时序角度

对于linearizability 系统,任意的两个操作的顺序都是可以比较的,即存在total order. 考虑:如果数据只有一份拷贝,同时操作又都是atomic的,那么任意两个操作总有先后关系,所以total order必然存在。

对比CAP-consistent

任意的一条读操作R,如果发生在某条写操作W完成之后(或执行过程中),那么R读到的要么是W的内容,要么是W之后的写操作写入的内容

这里的定义与CAP-consistent略有出入,为什么放宽限制为或执行过程中呢?因为定义之中所有的之前之后是否完成都是所谓上帝视角来判定的;对于client而言只有clients之间额外的交流沟通(参考后文),而对于clients之间额外的交流沟通而言,W完成与否也是无法判定的,考虑即使是执行W的client,也只能拿到W完成的响应时间,并不能真正知道server端W完成的时间(中间有网络延迟,物理时钟有误差等),即使利用因果关系进行clients之间额外的交流沟通也无从考证真正完成的时序。因此W是否真的完成并意义不大。

结合图示来看: 一旦有client读取到了写入的值,即使这个写入操作还没有完成,那么后续的读取操作都应该能读到该值或者之后写入的值

Why

  • 对于clients而言,一旦存在额外的交流沟通的渠道,linearizability问题就会凸显,例如:
    • A,B两个人去刷飞机票,A刷到了,B没有刷到(显示全部售光),如果A,B之间没有交流,即使B刷票先于A,则交易看起来也没有什么问题
    • 但如果A,B两个人存在交流,例如B没有刷到票,然后跑去隔壁房间问A,恰巧碰到A正在刷,并且刷票成功(B刷票 happened before A刷票),则交易存在问题
  • 如果能够提供linearizability的分布式系统,则:
    • 可以利用该系统实现分布式锁操作
    • 利用锁操作又可以用进行leader-election
    • 利用锁操作可以达成uniqueness guarantees
    • linearizability sequence number — 可以用来解决total order问题

How

如果可以保证分布式系统的各操作时序可比较(total order),则linearizability可达成;所以linearizability的实现问题可以转换成实现fault-tolent total order

而实现fault-tolent total oerder是一个distributed consensus问题

似乎就是一个循环: 如果实现了linearizability,则实现了linearizability sequence number,从而解决了total order问题,即实现了distributed consensus;而实现linearizability 又依赖通过distributed consensus实现total order

Weakness

linearizability is slow all the time, not only during a network fault(节点间通信达成共识本身就很耗时)

References

  1. Martin Kleppmann. 《Designing Data-Intensive Applications》9.Linearizability
]]>
<![CDATA[弱一致性]]> 2017-06-14T23:59:00+08:00 http://1feng.github.io/2017/06/14/weak-consistency Introduce

对于CAP而言,partition-tolerant是客观的必须要做的,不然不能称之为分布式系统;而consistent和available则是主观的, 应当根据业务需求适当调整的。相对于linearizability的强一致,其实还有多种弱一致性模型可以供系统设计时参考, 这里着重描述两种重要的一致性模型

Data-centric consistent models

Causal Consistency

与linearizability相同,causal consistency同样属于data-centric consistent models。与前者明显的区别在于,linearizability的系统的所有操作都存在total order,而causal consistency只需要partial order即可。

定义:

对于所有的进程看到的所有的写操作,都是因果相关的(causally related)且顺序相同。所有的读操作看到的结果也需要和写的因果顺序一致

如图:

两次写操作没有因果关系(concurrence),所以后续的两个client的读结果不相同,但这符合causal consistency的定义

How

实现causally related partital order即可,例如vector clock + causal order multicast protocol

Client-centric consistent models

Eventual Consistency

最终一致性比较好容易理解,很多primary-backup(asynchronous)架构的RDBMS都是使用的最终一致性模型

定义:

如果没有新的更新/写入,最终所有的clients都会看到最新的数据

最终是多久,不好说…

典型例子:

DNS系统

How

asynchronous log shipping + gossip protocal

References

  1. 《Distributed Systems An Algorithmic Approach Second Edition》16.3 16.4
]]>
<![CDATA[zab 算法总结]]> 2017-06-13T23:59:00+08:00 http://1feng.github.io/2017/06/13/zab Summary

zab是Yahoo提出的leader-base的一致性协议,由于raft晚于该协议猜测raft中有借鉴该协议的一些思想 此文仅总结理解的一些重点,并不完整总结该算法

FLP?

zab 中使用了timeout来进行故障检测,并没有突破FLP

Zxid

  • 高32位:代表epoch,与raft-term或multi-paxos的proposal number语意相同,与raft-term的不同点是自增的时机是在成为leader后
  • 低32位:自增id,等同与multi-paxos的instance-id/instance-index 或 raft-log-index

BroadCast

Zab broadcast依赖与FIFO(TCP)+ zxid 来保证消息的顺序(causal order + total order);paxos并不依赖于此而是靠proposal number来保证这一点;而raft则是通过log-index来保证的

Zab的broadcast本质就是放弃了abort动作的2PC协议,即:

  • 2PC中P1阶段可以由Participant选择YES or Abort,而Zab-BroadCast的P1阶段follower只能回复YES(即ACK),或者选择放弃该leader

Recovery

recovery 需要在正确性上保证以下两点:

  1. 不要忘记已经交付的消息
  2. 忽视应该跳过的消息(即leader 已经 broadcast,但是未获得多数派确认,后续leader又有新的提交,则该消息应该被忽视/放弃)

方法:

  • 选举leader时需保证leader拥有多数派认同的最大的zxid;与raft的log-up-to-date语意一致
  • 通过epoch来避免宕机恢复的leader提交应忽略的消息;与raft的term作用一致

Reference

[1]. A simple totally ordered broadcast protocol

[2]. ZooKeeper Internals

]]>
<![CDATA[Paxos made simple]]> 2017-06-12T23:59:00+08:00 http://1feng.github.io/2017/06/12/paxos-made-simple Summary

paxos算法的的核心思想是“与其预测未来,不如限制当下”,即通过保证当前的操作,来一步一步达到预期

Theory

要求

Safety:

  • 只有一个被提议的value被选定(validity)
  • 两个不同的进程不能做出不一样的的选择(agreement)

Liveness

  • 最终会有被提议的value被选定
  • 如果一个value被选定,任意进程最终一定会得知这一结果

推导过程

首先设定三个角色 proposers,acceptors,learners。

要想有value被选定,则acceptor必须要接受proposer的提议,于是我们要求

  • P1. 任意acceptor必须接受(accept)它收到的的第一个提议(proposal)

那么问题来了,多个proposers会提议多个value,无法满足safety. 于是我们考虑让acceptor可以接受(accept)多个提议,为了便于区分,我们考虑为提议增加一个total order的序号(proposal number),即提议由proposal number + value组成 但是最终我们是要选定(chosen)一个value的, 于是我们考虑可以接受多个提议,但是我们必须保证这些提议的value都是一样的,于是我们进一步要求:

  • P2. 如果value为v的提议被选定(chosen),则所有number更大的且被选定的提议的value也必须为v

一个提议如果被选定(chosen),那么至少被一个acceptor接受(accepted)过, 所以我们可以通过满足如下条件来达成P2

  • P2a. 如果value为v的提议被选定(chosen),那么所有number更大的且被任意acceptor接受过(accepted)的提议其value也必须是v

考虑一个acceptor c从没有收到提议,此时一个从故障中恢复的proposer发起了一个更高number的提议,且该提议与已经chosen的value不一样。按照P1,c肯定会accept该提议, 这样便违反了2a。于是我们强化一下P2a的要求

  • P2b. 如果value为v的提议被选定(chosen),那么由proposer发起的number更大的提议的value也必须是v

P2b通过限定proposer的动作来满足P2,通过归纳法我们可以得知,只要保证如下规则,就可以满足P2b

  • P2c. 那么对于大多数(majority)acceptors,我们称之为集合S;如果一个提议(n,v)被发起,则要么1成立,要么2成立
    • S中不存在acceptor接受过(accepted) number 小于n的提议
    • v是S接受过的(accepted)所有提议里number小于n的提议中number最大的提议的value

只要满足P2c就可以满足P2b,进而满足P2;至此我们便有了更具体的方式来实现P2c,具体如下:

  1. proposer选择一个proposal number n,然后向每个acceptors发起请求,要求acceptors:
    • 保证不再接受(accept)number小于n的提议,并且
    • 如果已经接受过(accepted)number小于n的提议,则这些提议中number小于n的最大的number以及该提议的value返回给proposer
  2. 如果proposer收到大多数(majority)的acceptors的响应,则proposer可以发起一个序号为number的提议,其value是v
    • v是所有acceptor响应的(mi, vi)中最大的m对应的v
    • 如果没有acceptor响应(mi, vi),则v由proposer自己决定

以上我们称之为PREPARE请求。利用PREPARE请求,我们完成了一个学习的过程,从而实现了P2c; proposal的具体实现我们归纳出来了,对应的acceptor的的要求也很容易得出:

  • 当且仅当(iff)acceptor 没有响应number大于n的prepare请求时,才可以接受(accept)number为n的提议

由于acceptor收到prepare请求后会保证不再接受(accept) proposal number小于n的提议,则acceptor便没有必要再回复proposal number小于n的prepare请求,我们可以直接忽略,或回复error或null使proposer放弃后续提议. 于是我们可以将proposer和acceptor的动作综合起来描述如下:

  • Phase 1
    • proposer生成一个proposer number n,然后发送prepare请求到所有(其实也可以是majority,但越多越能保证收到过半数的回复)acceptors
    • acceptor收到prepare请求后:
      • 如果之前有收到proposal number > n的prepare请求,则直接忽略该prepare请求,否则
      • 回复该prepare请求,同时如果之前有接受(accept)提议,则回复内容中带上接受的提议value和对应该value的最大的proposal number
  • Phase 2
    • proposer收到总数过半(majority)的回复后:
      • 如果所有回复中都没有携带提议value,则proposal自己选择一个提议value
      • 否则从所有回复中选择proposal number最大的的value
      • 向所有(其实也可以是majority,但越多越能保证收到过半数的accept)acceptor发送上述得到的提议value和proposer number n
    • acceptor收到提议请求后:
      • 如果之前没有回复proposal number > n的prepare请求,则接受(accept)该请求

以上可以完成总数过半的acceptor 接受(accept)一个value,但并不代表被chosen,该value被chosen需要:

  • 由learner来找出哪个提议(proposal number + value)被总数过半的的acceptors接受了(accepted),方式有如下:
    • 由接受(accept)提议的acceptor向所有learner发送通知消息,开销 M*N次通信(假设M个接受该提议的acceptor,N个learner)
    • 由接受(accept)提议的acceptor向某个learner发送通知消息,由该learner确定chosen结果后再广而告之,开销M+N次通信
    • 扩大方法二中某个learner为多个learner,适当增加开销,但可以保证可靠性(learner单点问题)

TO BE CONTINUE!

]]>
<![CDATA[三阶段提交]]> 2017-06-11T23:59:00+08:00 http://1feng.github.io/2017/06/11/3pc Why

1983年由Dale Skeen 和 Michael Stonebraker提出了3PC协议来解决2PC阻塞的问题

What

3PC(two-phase-commit)其实就是将2PC的Phase 2拆分成了两个阶段:

时序图

  • Phase 1:
    • Transaction coordinator(TC)首先写日志(write-ahead-log)记录事务执行状态,然后向所有Participants广播PREPARE消息,询问participant是否准备好commit(回复YES)或者选择abort(回复NO)
    • Participant收到PREPARE消息后,开始执行事务(考虑ACID-isolation,此时已经持有各种锁),如果执行中有任何问题则回复abort,如果事务执行完成则回复YES
    • TC收到所有的回复,进入Phase 2
  • Phase 2:
    • 如果TC收到的响应均为YES,则向participants广播PRE-COMMIT消息,否则广播ABORT消息(广播之前需更新日志,记录事务执行状态)
    • 如果participant收到PRE-COMMIT消息,回复ACK
    • 如果participant收到ABORT消息,终止事务
  • Phase 3:
    • 如果TC在超时时间内收到所有的ack,则向participants广播COMMIT消息,否则广播ABORT消息(广播之前需更新日志,记录事务执行状态)
    • Participant收到COMMIT/ABORT消息后,将事务正式commit/abort(考虑ACID-isolation,commit/abort完成后会释放所有锁)并回复ack

How

状态迁移图

来看异常处理的情况:

  • Phase 1:
    • Transaction coordinator(TC)发送PREPARE之后,如果超时时间内未收到响应,则放弃该事务,进入Phase 2 向所有participants广播ABORT
      • 此时收到ABORT的participants会正常终止事务
    • 当Participant收到PREPARE后,如果回复YES的时候超时(无法确定TC是否收到消息),retry几次后进入Phase 2
    • 当Participant收到PREPARE后,如果回复NO的时候超时(无论TC是否收到,TC都会进入Phase 2然后广播ABORT消息),重试几次之后可以主动终止事务
  • Phase 2:
    • TC发送了PRE-COMMIT/ABORT消息之后,如果长时间没有收到ack或者宕机重启之后都会进入Phase 3,发送ABORT消息
    • Participants如果长时间没有收到PRE-COMMIT消息,则可以主动终止事务
    • Participants如果收到PRE-COMMIT后,回复ack之前发生宕机,则可以主动终止事务
  • Phase 3:
    • TC发送了COMMIT/ABORT消息之后,如果长时间没有收到ack或者宕机重启之后都会根据write-ahead-log的内容重新发送消息,重试几次后结束(如果是发送COMMIT,则意味着TC认为事务已经完成;ABORT消息同理)
    • Participants如果长时间没有收到COMMIT/ABORT消息,执行commit

Weakness

3PC是一个理想状态的协议,假设fail-stop模型,并且可以通过timeout来准确判断网络故障还是宕机的情景(synchronous systems)下的协议(上文我们是按照真实环境来分析解析的)

  • 所以典型的一个3PC的冲突情景如下:  - Phase 2 TC 广播PRE-COMMIT消息,如果P1在收到消息前宕机,因而TC在Phase 3广播ABORT消息
    • 在Phase 2,P2回复ack之后进入Phase 3,并且与TC直接发生网络分区(network-partition)导致P2无法收到ABORT消息,故而自行决定commit
  • 网络通信需要3 RTT,开销较大

其他:

  • 标准的3PC假设的前提是理想状态,即fail-stop(the server only exhibits crash failures,且不恢复)模型
  • 标准的3PC描述Phase 3时,如果TC收到多数(majority)的ack,即可广播COMMIT(没有收到ack则意味着participant宕机且不恢复)
  • 根据以上两点,所以标准的3PC在synchronous systems(有限的timeout)下是可行的方案(上文的典型冲突情景不再发生)

PS:

  • 根据F·L·P定理在asynchronous system 模型下实现分布式共识是不可能的,但是实践之中我们能尽可能的去达成共识

Reference

[1]. D. Skeen and M. Stonebraker, “A Formal Model of Crash Recovery in a Distributed Systems,” IEEE Transactions on Software Engineering, SE-9, 3, (May 1983), pp. 219–228.

[2]. Sukumar Ghosh. 《Distributed Systems An Algorithmic Approach Second Edition》 14.5 Atomic Commit Protocols

[3]. Three-Phase Commit Protocol

[4]. Distributed Systems W4995-1 Fall 2014 lecture17

]]>
<![CDATA[两阶段提交]]> 2017-06-10T23:59:00+08:00 http://1feng.github.io/2017/06/10/2pc Why

针对数据库事务ACID-Atomicity,单机可以使用write-ahead-log实现1PC(one-phase-commit)即可,但是如果是分布式环境,考虑机器故障,网络不可靠1PC无法完成ACID-Atomicity

What

2PC(two-phase-commit)是已故图灵奖得主,事务处理领域大师Jim Gray提出的,用以解决分布式数据库事务ACID-Atomicity的一种共识(consensus)算法

  • Phase 1:
    • Transaction coordinator首先写日志(write-ahead-log)记录事务执行状态,然后向所有Participants广播PREPARE消息,询问participant是否准备好commit(回复YES)或者选择abort(回复NO)
    • Participant收到PREPARE消息后,开始执行事务(考虑ACID-isolation,此时已经持有各种锁),如果执行中有任何问题则回复abort,如果事务执行完成则回复YES
    • Transaction coordinator收到所有的回复,进入Phase 2
  • Phase 2:
    • 如果Ttransaction coordinator超时时间内收到的响应均为YES,则向participants广播COMMIT消息,否则广播ABORT消息(广播之前需更新日志,记录事务执行状态)
    • participant收到COMMIT/ABORT消息后,将事务正式commit/abort(考虑ACID-isolation,commit/abort完成后会释放所有锁)并回复ack

How

来看异常处理的情况:

  • Phase 1:
    • Transaction coordinator(TC)发送PREPARE之后,如果超时时间内未收到响应,则放弃该事务,进入Phase 2 向所有participants广播ABORT
      • 此时收到ABORT的participants会正常终止事务
    • 当Participant收到PREPARE后,如果回复YES的时候超时(无法确定TC是否收到消息),retry几次后进入Phase 2
    • 当Participant收到PREPARE后,如果回复NO的时候超时(无论TC是否收到,TC都会进入Phase 2然后广播ABORT消息),重试几次之后可以主动终止事务
  • Phase 2:
    • TC发送了COMMIT/ABORT消息之后,如果长时间没有收到ack或者宕机重启之后都会根据write-ahead-log的内容重新发送消息,直到收到ack为止(无限重试)
    • 一旦进入Phase 2,Participants会失去主动终止或提交事务的权利,只能等待TC发送的COMMIT/ABORT消息,亦或者主动发送get status消息
    • 事务是有一个全局唯一的事务ID唯一确认的,这一点可以确保TC重新发送COMMIT/ABORT消息时恢复连接的participant可以识别并回复ack

Weakness

2PC is a blocking protocol

由于TC宕机或者与部分participant断开连接(或者Participant宕机),则意味着阻塞(blocking),直到宕机恢复网络恢复为止。

以TC宕机为例,考虑ACID-isolation 这会导致participant长时间持有lock而不释放,影响participant可用性

Reference

[1]. Martin Kleppmann. 《Designing Data-Intensive Applications》9.Consistency and Consensus

[2]. Sukumar Ghosh. 《Distributed Systems An Algorithmic Approach Second Edition》 14.5 Atomic Commit Protocols

[3]. Notes on Data Base Operating Systems. Jim Gray. IBM Research Laboratory. San Jose, California. 95193. Summer 1977

]]>
<![CDATA[分布式系统的正确性]]> 2017-06-09T23:59:00+08:00 http://1feng.github.io/2017/06/09/correctness Introduce

一般正确性的证明标准有两个,分别是safety properties 和 liveness properites

Safety Properites

通常safety properites是指:“bad things never happen”。

举例

例如互斥操作(不管单机还是分布式)的safety properites可以是: - 最多只能有一个process or thread进入临界区 - 至少有一个process or thread有资格进入临界区

Liveness Properites

通常liveness properites是指:”good things eventually happen”。

对应现实世界的一个例子就是“正义终将来临”,至于具体什么时候,不太好说。

liveness properites的描述经常带有”eventually”字样,例如eventually consistency就是liveness properites。

举例

例如互斥操作(不管单机还是分布式)的liveness properites可以是: - 每个试图进入临界区的process or thread最终都将进入临界区 - 至少有一个process or thread有资格进入临界区

Proof

常见的证明方式暂时未做了解 ☻

]]>
<![CDATA[CAP 问题]]> 2017-06-08T23:59:00+08:00 http://1feng.github.io/2017/06/08/cap Introduce

于2002年提出的CAP理论(三选二的方式来评估分布式系统)确实为分布式系统领域的发展提供了指导价值,但是就今天而言,这套理论已经意义微小了

Consistent

这里的一致性指的是强一致,又称linearizable或atomic。

论文中的描述如下:

Under this consistency guarantee, there must exist a total order on all operations such that each operation looks as if it were completed at a single instant.

简单来讲就是如果把分布式系统看做一个黑盒,在外部看起来这个系统就是和单机没有区别。

具体的来说:

任意的一条读操作R,如果发生在某条写操作W完成之后,那么R读到的要么是W的内容,要么是W之后的写操作写入的内容

更详细的描述可以参考linearizable

这里的consistent 和 ACID中的consistent是完全不同的概念: - ACID-consistent特指事务 - CAP-consistent仅仅是请求/响应操作顺序的属性

Available

论文中的定义:

For a distributed system to be continuously available, every request received by a non-failing node in the system must result in a response

这里的response是指no-error response

即使是Probabilistic availability,在任意的failures发生时也不会影响针对CAP-available的结论,但是这里为了简单起见特指100% availability。

如果专门针对partition-tolerance而言的话,available可以描述为:

even when severe network failures occur, every request must terminate.

terminate 是指任意使用该分布式系统的算法都会终止,注意是算法的终止。

Partition Tolerance

网络割接和交换机故障都会造成network partition

network partition 图示:

CAP的问题也是从这里开始体现:

  • partition tolerance并非和CA对等的属性,而是一种因果的关系:partition发生时是选A还是选C,即如何去tolerant partition,
  • 分布式系统需要考虑的其他网络问题也很多,包括延迟,网络不可靠等,并不仅仅是partition,所以使用CA,CP,AP去描述一个分布式系统并不完整
  • 很多分布式系统可以根据业务需求降低对consistent的要求,降低对available的要求,所以根本无法用CAP来描述

Partition in practice

尽管network partition不能涵盖分布式系统所有需要面对的网络问题,但是它确实是网络问题中的一个难点和重点

single-leader-Architecture

当某个client和leader处于不同partition时,此时CAP-available丢失,如果按照CAP理论,只能称之为CP

multi-leader-Architecture

情景一

某个client和所有的leader都不在一个partition,此时CAP-available丢失,如果按照CAP理论,只能称之为CP

如果你允许(业务上允许)图示中的client2对replica进行read操作,则CAP-consistent也会丢失,只能称之为P(CAP的3选2现在成了3选1)

情景二

leaders不在一个partition,此时CAP-consistent丢失,如果按照CAP理论,只能称之为AP

dynamo-style-Architecture(no-leader)

R + W > N,但是当network partition发生时,如果某个client被划分到了节点较少的一侧,那么CAP-available丢失,只能称之为CP;

如果你允许(业务上允许)图示中的client2进行read操作,则CAP-consistent也会丢失,只能称之为P(CAP的3选2现在成了3选1)

References

  1. Martin Kleppmann. please-stop-calling-databases-cp-or-ap
  2. Martin Kleppmann. 《Designing Data-Intensive Applications》9.Linearizability
]]>
<![CDATA[What is ACID]]> 2017-06-07T23:59:00+08:00 http://1feng.github.io/2017/06/07/ACID What

Atomicity

描述

一个事务包含一系列的操作,这一系列的操作都成功,则意味着事务执行成功;一旦执行过程中发生故障(fault),数据库需要放弃整个事务,并且撤销已经完成的部分操作

优势

方便异常处理,如果事务终止,应用层面可以确保什么修改都没有发生,可以安全的重试

典型案例

A向B账户转账100元: 1. 从A的账户减少100元 2. 从B的账户增加100元

如果1执行完成2还未执行,此时数据库故障(system fails),则为了保证Atomicity,数据库的事务系统需要回滚1操作

其他

这里需要与concurrency-atomic做一下区分, concurrency-atomic指的是当某个线程执行某个操作时,其他线程不可能看到中间状态(half-finished)

Consistency

描述

这里的consistency是指,当事务结束时,系统(数据库)处于一个合法的状态(valid state),也就是说系统总是从一个合法的状态迁移至另一个合法的状态

其他

  1. ACID-consistency是一个比较模糊的概念,状态迁移是系统的用户来保证的,系统只能保证其中一部分,不能完全覆盖,所以consistency依赖用户而不是系统
  2. MSDN给出的例子[2]和Atomicity类似,但是差别在于A中事务终止回滚时因为system fails,而C中事务终止回滚是因为error(比如类型不匹配,数字和字符串做加法?)
  3. ACID-consistency 和CAP-consistency直接没有任何关系,仅仅使用了同一个单词而已

Islation

描述

Isolation是指当多个事务并发(concurrency)执行时,应该彼此之间存在隔离,执行过程中互不影响

Durability

描述

一旦事务成功提交,即使发生硬件故障或者程序崩溃,任何已经写入的数据都不能丢失

How

Atomicity ★★★★

可以利用持久化日志来实现,方便重启回滚

Consistency ★★

数据库层面做足够的合法性检测,其他由用户层/应用层来保证

Islation ★★★★★

先看几点要求

  • Read commited(weak-islation type) 的两点要求
    • No Dirty Read: 不会读取到其他正在执行的事务中间状体的数据
    • No Dirty Write: 事务不会overwrite到其他事务的uncommitted的数据
  • No Read Skew:
    • Read Skew举例:
      • A 在两个账户中各存放了500块钱,现在A要查询两个账户的余额
      • 查询账户1的SQL执行完成,余额500
      • 假设A之前设置了一笔定时的自动转账被触发,从账户2向账户1转100块,事务执行成功,账户1余额600,账户2余额400
      • 查询账户2的SQL执行完成,余额400
      • 在A看来,账户总额少了100块
      • 即使如此这个场景还是可以接受的,因为A可以重新查询,即可获得正常结果
    • 无法接受Read Skew的两个场景:
      • Backup
        • 事务执行的同时,可以完成数据备份
      • Analytic Queries and Integrity checks
        • 事务执行的同时, 需要完成大量数据的查询或扫描
  • Read-Modify-Write / Atomic Write Operation
    • 举例:两个用户同时对一个counter字段做inc操作,后果与多线程并发操作类似会丢失一部分inc操作
  • Write Skew
    • 举例(针对multi-object的场景):
      • 两位医生Alice 和 Bob同时检查当前是否有另外一个人正在值班,如果有则在系统中停止自己的值班状态,然后回家睡觉
1
2
3
4
5
6
7
8
9
10
11
12
13
Alice执行事务如下:
currently_on_call = (select count(*) from doctors where on_call = true and shift_id = 1234)
if (currently_on_call >= 2) {
    update doctors set on_call = true where name=Alice and shift_id = 1234
}

Bob执行事务如下:
currently_on_call = (select count(*) from doctors where on_call = true and shift_id = 1234)
if (currently_on_call >= 2) {
    update doctors set on_call = true where name=Bob and shift_id = 1234
}

有点像是multi-object版本的read-modify-write,但是有本质区别

解决方案

  • Read commited
    • Dirty Write: 可以使用row-level lock来避免dirty write
    • Dirty Read:
      • 同样可以使用row-level lock来避免dirty read,但是缺点在于一个比较耗时的写操作会阻塞住read-only的操作,更严重的是会因此引发连锁反应
      • 更好的解决方法是使用类似于MVCC的snapshot-isolation方案来解决dirty read的问题
  • No Read Skew
    • 类似于MVCC的snapshot-isolation方案来解决read skew问题,可同时满足Backup和Analytic Queries and Integrity checks的需求
  • Read-Modiry-Write / Atomic Write Operation
    • 使用显示的锁操作(explicit-locking)来实现atomic write operation
    • automatically-detecting-lost-update,一旦检测到lost update,事务需要终止并且retry
    • 实现compare-and-set操作用以支持SQL-where语句
  • Write Skew
    • 串行化(serializability)隔离所有事务,这种方式可以解决上述除read skew外所有问题,但是工程实现上往往性能会是一个非常大的问题

通常为了实现isolation,都是综合以上各种方案

Durability ★★★★

磁盘+replica

Serializability

What

serializable-isolation 是最强等级的事务并发隔离,他可以确保即使多个事务是并行(parallel)执行的,最终的结果看起来也像是顺序的(serially),每个时间点只有一个事务在执行

How

根据上述描述,不难看出,其要求是让数据库解决所有的可能的并发竞争问题

  • 真的串行化的执行事务:
  • 方法:将所有的事务扔到一个队列里排队,由特定的线程来依次执行
  • 缺点:性能太差
  • 存储过程(stored procedures)+ in-memory data:
  • 解释:本质是加快单个事务的执行速度(没有了磁盘IO),以便可以真正串行化事务执行
  • 缺点:存储过程需要用户自己来用SQL/PL完成,调试测试监控都比较棘手,同时一旦用户完成的存储过程性能比较差,会造成恶劣的影响,甚至引发连锁反应
  • 数据分区(partitioning)
  • 解释:本质是将单机的性能问题通过scale out来加速
  • 缺点:事务执行涉及的数据不能跨分区
  • Two-Phase-Locking(2PL)
  • 描述:
    • 当事务需要读一个object时,必须先以shared mode获取锁;多个事务可以同时以shared mode获取锁,但是一旦有事务以exclusive mode持有了锁,其他事务必须等待
    • 如果事务想要写一个object,必须先以exclusive mode获取锁;区别于shared mode,同一时间只能有一个事务以exclusive mode持有锁
    • 如果事务先读一个object,然后又要写(read-modify-write),则需要将锁从shared mode升级为exclusive mode
    • 一旦事务获取了锁,除非事务提交或者终止,否则不允许释放锁,这也是二阶段命名的由来;
  • 解释:
    • Expanding phase(扩大阶段–事务执行中): locks are acquired and no locks are released.
    • Shrinking phase(收缩阶段–事务结束时): locks are released and no locks are acquired.
  • 缺点:
    • 吞吐量(through-put) 和 响应时间 与仅实现weak-isolation(如read-commit + No Read Skew)相比会比较差
    • deadlock风险增大
  • Serializable Snapshot Isolation(SSI)
  • 与之前提到的snapshot-isolation相比,SSI为写操作增加了串行(serialization)冲突检测
    • detecting stale MVCC reads:针对write skew,如果事务提交时检测到之前的前置条件已经不成立了,则终止事务
    • detecting writes that affect prior read:同样考虑write skew,数据库从index-level/table-level保存一些信息,以便当事务提交后可以检测其操作是否造成其他正在执行的事务读取的数据过期(前置条件失效),如果存在则主动通知该事务终止

Serializability VS Linearizability

  • serializability: 事务隔离的属性,指事务执行的结果看起来像顺序的(串行的),以避免write skew
  • linearizability: 指对读写共享数据的新近性(recency guarantee),与事务(把一系列操作看做整体来讨论)无关

References

[1]. Martin Kleppmann. 《Designing Data-Intensive Applications》7.Transactions

[2]. ACID properties

[3]. Linearizability versus Serializability

]]>
<![CDATA[unreliable network]]> 2017-06-06T23:59:00+08:00 http://1feng.github.io/2017/06/06/unreliable-network Introduce

众所周知TCP是可靠的网络传输协议,但是为什么在分布式系统中又认为网络是不可靠的呢?通常有以下两点: 1. 发送方无法确定接收方已经收到请求 2. 发送方无法无法知晓接收方是否处理完请求

可以看出,以上指的都是从应用层的角度观察的结果,而引起以上问题的原因可能有: - 消息在路由队列中等待转发 - 接收方队列满,发生丢包 - 接收方处理完成,回复的消息在排队或发生丢包 - gc-stop-the-world等

Synchronous network

像电话网络,有线电视网络等都是所谓synchronous network,他的特点如下:

  • 一旦连接建立,即享用专线

  • 专线享有固定的带宽

  • 路由(routers)没有队列

以上决定了synchronous network的最大网络延迟是固定有上限的,即可以用timeout来判断消息传输是否存在问题

Asynchronous network

既然有synchronous network为什么还要搞以太网这一套呢?原因是为了充分利用带宽,由于互联网上数据传输的大小都不是固定的,使用专线意味着带宽资源的浪费。

因此,Ehernet && ip 使用了packed-switched协议, 具体如下:

  1. 路由引入队列,最大化线路使用率

  2. TCP层引入send buffer && recv buffer来动态的适配数据传输速率(滑动窗口)

上述的优化本质是在latency和resource utilization之间做trade-off,也因此导致了无上限的延迟时间,即无法选择一个合适的timeoout来进行传输故障检测

]]>
<![CDATA[timing and order]]> 2017-06-05T23:59:00+08:00 http://1feng.github.io/2017/06/05/timing-and-order Introduce

分布式环境面临的两个主要的问题就是网络不可靠和时钟不可靠,这里主要总结时钟问题

Physical Clocks

我们日常使用的计算机和服务器的物理时钟都是使用的石英(quartz)时钟,这类时钟天生存在误差,虽然铯原子钟的精度更高但是造价昂贵,并不适合商用计算机。

对于商用计算机的时钟误差,通常使用NTP协议来进行时钟同步,然而由于网络的不可靠以及时钟误差NTP同步也会有些问题。

商用计算机利用时英时钟在计算机上实现了两种clock:

  • wall clock

    • 受NTP同步的影响,时钟会jump forward 或 jump backward来完成时钟同步
    • 如linux上的int gettimeofday(struct timeval tv, struct timezone tz);,返回1970-01-01 00:00:00 +0000 (UTC)至今的秒数和豪秒数
  • monotonic clock

    • 不受NTP影响,或者,受NTP同步的影响,时钟只会降低或者升高频率,以尽快完成时钟同步
    • 如linux上的int clock_gettime(clockid_t clk_id, struct timespec *tp),clk_id为CLOCK_MONOTONIC_RAW(本质是jiffies)或者是CLOCK_MONOTONIC;分别对应上述不受NTP影响和受NTP影响两种

适用性:

  • wall clock
    • 适用于:
      • 单机保证时序
    • 不适用:
      • 单机计算duration或elapsed time,例如统计timeout,expire
      • 分布式环境下的时序问题
  • monotonic clock
    • 适用于:
      • 单机计算duration或elapsed time,例如统计timeout,expire
      • 单机保证时序
    • 不适用:
      • 分布式环境下时序问题

那么分布式环境下的时序问题如何解决呢?

  • 全序(total order)或者高精度的时间点共识(强调某个时间点):
    • 使用原子钟加更严格复杂的时钟同步策略来保证误差
    • fault-tolent total ordering broadcast
  • 偏序(partial order):
    • 利用因果关系来解决时序问题,即logic clock

Logic Clock

利用因果关系来实现Logic Clock,见Lamport的论文

利用Logic Clock来保证时序(偏序),见 Vector Clock

其他

一个错误使用wall clock的案例

References

  1. 《Designing Data-Intensive Applications》8.Unreliable Clocks
  2. How to do distributed locking
  3. 《Time-Clocks-and-the-Ordering-of-Events-in-a-Distributed-System》
  4. Vector Clock
]]>
<![CDATA[vector clock summary]]> 2017-06-05T00:59:00+08:00 http://1feng.github.io/2017/06/05/vector-clock Summary

Happend before

用→来表示hanppend before,对于任意event a, b 有:

  1. 如果a和b属于同一个process,并且a comes before b, 则 a → b
  2. 如果a是某个process发送信息的event,b是另一个process接收该信息的event,那么 a → b
  3. 如果 a → b并且 b → c,那么 a → c

以上本质是基于一个因果关系(causality)来定义的hanppend before

Summary

Why

Lammport Clock(Logical Clock) 只能通过因果关系推断其Logical Clock的关系,即:

  • 如果a → b, 则C(a) < C(b), 反过来并不一定成立(其实就是事后诸葛亮,事件先发生才产生因果关系),同时:
    • 同一个process上的两个事件由a → b 得到C(a) < C(b),
    • 但是a,b可能因为和另一个process上的事件c没有因果关系处于并发状态
    • 但是按照Lammport的描述的Logic Clock的实现,C(c)很有可能满足 C(a) < C(c) < C(b)
    • 然而实际情况是c和a,c和b均无因果关系

Vector Clock的出现就是为了解决上述问题。

What

假设有n个processes,V为n个processes上的事件集合,a,b∈V;

对于vector clock 如果VC(a) < VC(b),仅且仅当:

  • ∀i: 0 <= i <= n - 1: VCi(a) <= VCi(b)
  • ∃j: 0 <= j <= n - 1: VCj(a) < VCj(b)

通俗的讲就是向量维度匹配并且VC(a)的所有维度都不大于VC(b)并且至少有一个维度小于VC(b),这时候VC(a) < VC(b)

同时:VC(a) < VC(b) <==> a → b

How

processes编号0–n-1, VC利用数组实现,下标从0到n-1,初始为[0,0,0…0]

  1. 对于process i,本地的VC为VCi,对于任意事件发生后 ++VCi[i]
  2. 当i向其他process发送数据时,带上本地的VCi
  3. 当process j接收到VCi时
  4. ++VCj[j]
  5. ∀k : 0 <= k <= n - 1: VCj[k] = max(VCi[k], VCj[k])

Weakness

  1. partial order not total order
  2. 无法满足VC(a) < VC(b)时还是无法解决order问题。dynamo论文中的提到的处理方式是将该问题抛给client根据业务处理(PS:dynamo据说已经不用vector clock了)
  3. vector size 随着processes数量线性增长  - Riak开发者提供了一种解决方案,在vector clock中带上各自processes的本地time stamp,当vector size到达指定的阈值后,删除最旧的process在vector clock中的数据;这样造成的问题就是丢失了和最旧的process的因果关系,按照作者的说法,好在这并不会造成数据丢失,just a tradoff!

References

  1. Vector Clock In Wikipedia
  2. 《Distributed Systems An Algorithmic Approach Second Edition》 6.3 Vector Clock
  3. Why Vector Clocks Are Hard
]]>
<![CDATA[《Time, clocks, and the ordering of events in a distributed system》summary]]> 2017-06-03T23:59:00+08:00 http://1feng.github.io/2017/06/03/Time-Clocks-and-the-Ordering-of-Events-in-a-Distributed-System Summary

Happend before

用→来表示hanppend before,对于任意event a, b 有:

  1. 如果a和b属于同一个process,并且a comes before b, 则 a → b
  2. 如果a是某个process发送信息的event,b是另一个process接收该信息的event,那么 a → b
  3. 如果 a → b并且 b → c,那么 a → c

以上本质是基于一个因果关系(causality)来定义的hanppend before

concurrence意味着a → b不成立并且b→a也不成立,即a,b之间缺少因果关系

b → c 并且 a → c, 但是a,b并不能推导出因果关系,因此happend before是partial order. 同时由于a → a不成立,所以happend before是反自反(irreflexive)的partial order

logical clocks

定义Ci(b)为event b在process i 上发生时的clock。

对于任意的events a,b:

如果a → b,则C(a)< C(b)

显而易见:

  1. 如果a,b同属于process Pi, 并且 a comes before b, 则C(a) < C(b)
  2. 如果a是Pi上发送信息的event,b是Pj上接收该信息的event,那么Ci(a) < Cj(b)

具体实现:

  1. 对于任意Pi在两个successive event之间会增加Ci, Ci += 1
  2. 以下
  3. a. 如果a是Pi上发送信息的event,信息m包含一个时间戳Tm = Ci(a)
  4. b. 当Pj收到信息m,设置Cj = max(Cj, Tm) + 1

Logical Clock 的缺点:a, b可能同时发生,C(a) < C(b)并不能推断出a → b

total ordering

In mathematics, a linear order, total order, simple order, or (non-strict) ordering is a binary relation on some set X, which is antisymmetric, transitive, and total. A set paired with a total order is called a totally ordered set, a linearly ordered set, a simply ordered set, or a chain. —- from wikipedia

定义关系=>如下:

如果a属于Pi,b属于Pj,a => b当且仅当要么Ci(a) < Ci(b)要么Ci(a) = Ci(b) 并且Pi < Pj

Pi < Pj可以是process name 字典序或者数字标示的顺序。

total ordering强调对于任意两个元素都有可比性

paper中举例使用no-fault-tolent total ordering解决分布式情况下mutual exclusion的问题

值得特别强调的一点,这里的total ordering和hanppend before没有关系,但是total ordering的意义在于可以用在例如mutual exclusion场景,用顺序来保证fairness(一般的mutual exclusion的关系是FIFO来保证fairness的)

Anomalous Behavior

例如:

  1. event a : P 发送消息到R
  2. event b : P发送消息到Q,Q将消息转发给R

对于P而言 a→b,但是由于网络延迟R就不一定这么认为了。

解决方法有两种:

  1. 发送的消息中带上logical clock
  2. 利用Physical Clock

Physical Clocks

大概介绍了什么样(主要指同步)的physical clock可以用来解决上述的问题。

References

  1. wikipedia
  2. 《Distributed Systems An Algorithmic Approach Second Edition》 6.2 Logical Clock
  3. 《Distributed Systems An Algorithmic Approach Second Edition》 7.2 Solutions On Message-Passing Systems
]]>
<![CDATA[leveldb源码笔记之读操作]]> 2016-09-10T22:07:00+08:00 http://1feng.github.io/2016/09/10/leveldb-read key逻辑分类

根据我们之前文章的描述,leveldb的数据存储可能存在在内存的memtable中,或者磁盘的sstalbe中,但是key的实际存储格式会略微有差异,代码里按照存储的位置,划分为以下几种类型:

memtable: 逻辑上称为memtable_key

sstalbe: 逻辑上称为internal_key

key: 用户提供的key,我们称之为user_key

当用户去查询某个key时,leveldb会先利用key构建起Lookupkey类

Lookupkey类内部的完整数据即memtable_key,可以方便的利用成员函数截取memtable_key,internal_key,user_key以方便去memtalble和sstable中查询

事实上LookupKey是由 key, sequence number组成的,如之前文章提到:

  • 如果普通Get()操作,sequence number 为 last sequence number
  • 如果是使用的snapshot, sequence number 为 snapshot sequence number
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
31
32
33
34
35
36
37
38
39
40
// dbformat.h
// lookup key format:
// start_       kstart_                                         end_
//   |             |                                             |
//   |             |<--user_key-->|                              |
//   |             |<---------------internal_key---------------->|
//   |<---------------------memtable_key------------------------>|
//   -------------------------------------------------------------
//   |  1--5 byte  | klenght byte |           8 byte             |
//   -------------------------------------------------------------
//   | klenght + 8 |   raw key    | pack(sequence number, type)) |
//   -------------------------------------------------------------
// A helper class useful for DBImpl::Get()
class LookupKey {
 public:
  // Initialize *this for looking up user_key at a snapshot with
  // the specified sequence number.
  LookupKey(const Slice& user_key, SequenceNumber sequence);

  ~LookupKey();

  // Return a key suitable for lookup in a MemTable.
  Slice memtable_key() const { return Slice(start_, end_ - start_); }

  // Return an internal key (suitable for passing to an internal iterator)
  Slice internal_key() const { return Slice(kstart_, end_ - kstart_); }

  // Return the user key
  Slice user_key() const { return Slice(kstart_, end_ - kstart_ - 8); }

 private:
  const char* start_;
  const char* kstart_;
  const char* end_;
  char space_[200];      // Avoid allocation for short keys

  // No copying allowed
  LookupKey(const LookupKey&);
  void operator=(const LookupKey&);
};

如图:

读操作

图示Get()操作的基本逻辑如下: 以上我们是假设sstable没有filter的情况下的操作逻辑

cache

无论是table cache,还是block cache,都是使用了相同的数据结构LRUCache来实现的,区别只在于内部存储的数据不同。

LRUCache是通过k/v方式存储的,对于:

TableCache:

  • key: 其实就是file number
1
2
3
4
// table_cache.cc
char buf[sizeof(file_number)];
EncodeFixed64(buf, file_number);
Slice key(buf, sizeof(buf));
  • value: TableAndFile, 其实主要是sstable index block里的数据
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
// table_cache.cc
struct TableAndFile {
  RandomAccessFile* file;
  Table* table;
};

// table.cc
// Table里的主要数据即下述
struct Table::Rep {
    ~Rep() {
      delete filter;
      delete [] filter_data;
      delete index_block;
    }

    Options options;
    Status status;
    RandomAccessFile* file;
    uint64_t cache_id;
    FilterBlockReader* filter;
    const char* filter_data;

    BlockHandle metaindex_handle;  // Handle to metaindex_block: saved from footer
    Block* index_block;
};

BlockCache:

  • key: 其实是 cache_id 和 block 在sstable中的offset的组合
1
2
3
4
5
6
// table.cc
char cache_key_buffer[16];
// 构造block_cache 的key
EncodeFixed64(cache_key_buffer, table->rep_->cache_id);
EncodeFixed64(cache_key_buffer+8, handle.offset());
Slice key(cache_key_buffer, sizeof(cache_key_buffer));
  • value: data block 内容
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
// block.h
class Block {
 public:
  // Initialize the block with the specified contents.
  explicit Block(const BlockContents& contents);

  ~Block();

  size_t size() const { return size_; }
  Iterator* NewIterator(const Comparator* comparator);

 private:
  uint32_t NumRestarts() const;

  const char* data_;
  size_t size_;
  uint32_t restart_offset_;     // Offset in data_ of restart array
  bool owned_;                  // Block owns data_[]

  // No copying allowed
  Block(const Block&);
  void operator=(const Block&);

  class Iter;
};

cache 逻辑结构图示

]]>
<![CDATA[leveldb源码笔记之Compact]]> 2016-09-06T14:55:00+08:00 http://1feng.github.io/2016/09/06/leveldb-compact 简介

leveldb中只有minor compaction 和 major compaction两种

  • 代码中通过调用DBImpl::MaybeScheduleCompaction()来触发两种compaction
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
// db_impl.cc
void DBImpl::MaybeScheduleCompaction() {
  mutex_.AssertHeld();
  // 确保只有一个后台线程在做compact
  if (bg_compaction_scheduled_) {
    // Already scheduled
  } else if (shutting_down_.Acquire_Load()) {
    // DB is being deleted; no more background compactions
  } else if (!bg_error_.ok()) {
    // Already got an error; no more changes
  } else if (imm_ == NULL &&
             manual_compaction_ == NULL &&
             !versions_->NeedsCompaction()) {
    // No work to be done
  } else {
    bg_compaction_scheduled_ = true;
    // 启动compact线程,主要逻辑是通过DBImpl::BackgroundCompaction()实现
    env_->Schedule(&DBImpl::BGWork, this);
  }
}

调用时机:

  • 1.每次写入前,需要确保空间充足,如果空间不足,尝试将memtable转换为immutable-memtable,之后调用DBImpl::MaybeScheduleCompaction()
  • 2.每次重启db,binlog recover结束后,会触发调用DBImpl::MaybeScheduleCompaction()
  • 3.每次读取一条记录结束时会触发调用DBImpl::MaybeScheduleCompaction()

minor compaction:

方式:

  • 将immutalbe-memtable dump到磁盘,形成sstable
  • sstable一般位于level-0,如果sstable的key范围和当前level没有重叠会尝试下移,最多不会超过config::kMaxMemCompactLevel(默认为2)

触发时机:

  • 每次调用BackGroudCompaction如果存在immutalbe-memtable都会触发将其dump到磁盘

major compaction

方式:

  • 将level-n的sstable 与 level-(n+1)中与之存在key范围重叠的sstable多路归并,生成level-(n+1)的sstable
  • 如果是level-0,则由于level-0中sstable之间key有重叠,所以level-0参与compact的sstable可能不止一个

触发时机:

第一种是size触发类型(优先):

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
31
32
33
34
35
36
37
38
39
40
// version_set.cc
void VersionSet::Finalize(Version* v) {
  // Precomputed best level for next compaction
  int best_level = -1;
  double best_score = -1;

  for (int level = 0; level < config::kNumLevels-1; level++) {
    double score;
    if (level == 0) {
      // We treat level-0 specially by bounding the number of files
      // instead of number of bytes for two reasons:
      //
      // 对于较大的write buffer, 不过多的进行levle-0的compactions是好的
      // (1) With larger write-buffer sizes, it is nice not to do too
      // many level-0 compactions.
      //
      // 因为每次读操作都会触发level-0的归并,因此当个别的文件size很小的时候
      // 我们期望避免level-0有太多文件存在
      // (2) The files in level-0 are merged on every read and
      // therefore we wish to avoid too many files when the individual
      // file size is small (perhaps because of a small write-buffer
      // setting, or very high compression ratios, or lots of
      // overwrites/deletions).
      score = v->files_[level].size() /
          static_cast<double>(config::kL0_CompactionTrigger);
    } else {
      // Compute the ratio of current size to size limit.
      const uint64_t level_bytes = TotalFileSize(v->files_[level]);
      score = static_cast<double>(level_bytes) / MaxBytesForLevel(level);
    }

    if (score > best_score) {
      best_level = level;
      best_score = score;
    }
  }

  v->compaction_level_ = best_level;
  v->compaction_score_ = best_score;
}
  • 对于level-0:

    • score = level-0文件数/config::kL0_CompactionTrigger(默认为4)
  • 对于level-n(n>0):

    • score = 当前level的字节数 / (10n * 220) 220 即1MB
  • score >= 1,当前level就会被标识起来,等待触发 compaction

第二种是seek触发:

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
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
// version_edit.h

// 记录了文件编号, 文件大小,最小key,最大key
// sstable文件的命名就是按照file number + 特定后缀完成的
struct FileMetaData {
  int refs;
  int allowed_seeks;          // Seeks allowed until compaction
  uint64_t number;
  uint64_t file_size;         // File size in bytes
  InternalKey smallest;       // Smallest internal key served by table
  InternalKey largest;        // Largest internal key served by table

  FileMetaData() : refs(0), allowed_seeks(1 << 30), file_size(0) { }
};

// version_set.cc

// Apply all of the edits in *edit to the current state.
void Apply(VersionEdit* edit) {
  ...
  for (size_t i = 0; i < edit->new_files_.size(); i++) {
    const int level = edit->new_files_[i].first;
    FileMetaData* f = new FileMetaData(edit->new_files_[i].second);
    f->refs = 1;
    // We arrange to automatically compact this file after
    // a certain number of seeks.  Let's assume:
    //   (1) One seek costs 10ms
    //   (2) Writing or reading 1MB costs 10ms (100MB/s)
    //   (3) A compaction of 1MB does 25MB of IO:
    //        1MB read from this level
    //        10-12MB read from next level(boundaries may be misaligned)
    //        10-12MB written to next level
    // This implies that 25 seeks cost the same as the compaction
    // of 1MB of data.  I.e., one seek costs approximately the
    // same as the compaction of 40KB of data.  We are a little
    // conservative and allow approximately one seek for every 16KB
    // of data before triggering a compaction.
    // 1次seek相当与compact 40kb的data,
    // 那么n次seek大概和compact一个sstable相当(n = sstable_size / 40kb)
    // 保守点,这里搞了个16kb
    f->allowed_seeks = (f->file_size / 16384);  // 2^14 == 16384 == 16kb
    if (f->allowed_seeks < 100) f->allowed_seeks = 100;
    ...
  }
  ...
}
  • 当一个新的sstable建立时,会有一个allowed_seeks的初值:
    • 作者认为1次sstable的seek(此处的seek就是指去sstable里查找指定key),相当于compact 40kb的数据,那么 sstable size / 40kb 次的seek操作,大概和compact 一个 sstable相当
    • 保守的做法,allowed_seeks的初值为file_size/16kb
    • 如果allowed_seeks小于100,令其为100
  • 每当Get操作触发磁盘读,即sstable被读取,该数值就会减一;如果有多个sstable被读取,则仅首个被读取的sstable的sllowed_seeks减一
  • allowed_seeks == 0 时,该sstable以及其所处level会被标识起来,等待触发 compaction

sstable选择:

  • 针对size触发类型,默认从当前level的首个sstable开始执行

  • seek触发相对简单,sstable已经选择好了

  • 对于level-0,需要将与选中的sstable存在key重叠的sstable也包含进此次compact

  • 对于level-(n+1),需要将与level-n中选中的sstable存在key重叠的sstable包含进此次compact

    由于level-(n+1)多个sstable的参与扩展了整个compact的key的范围, 我们可以使用该key范围将level-n中更多的sstable包含进此次compact 前提是保证level-n更多sstable的参与不会导致level-(n+1)的sstable数量再次增长. 同时,参与整个compaction的字节数不超过kExpandedCompactionByteSizeLimit = 25 * kTargetFileSize = 25 * 2MB;

  • 为了保持公平,保证某个level中每个sstable都有机会参与compact:

    • 存储当前level首次compact的sstable(s)的largest key,存入compact_point_[level]
    • 当前level如果再次被size触发进行compact时,选择首个largest key大于compact_point_[level] sstable进行compact
]]>
<![CDATA[leveldb源码笔记之MVCC && Manifest]]> 2016-08-24T15:51:00+08:00 http://1feng.github.io/2016/08/24/mvcc-and-manifest MVCC

问题: 针对同一条记录,如果读和写在同一时间发生时,reader可能会读取到不一致或者写了一半的数据

常见解决方案

悲观锁:

最简单的方式,即通过锁来控制并发,但是效率非常的低,增加的产生死锁的机会

乐观锁:

它假设多用户并发的事物在处理时不会彼此互相影响,各食物能够在不产生锁的的情况下处理各自影响的那部分数据。在提交数据更新之前,每个事务会先检查在该事务读取数据后,有没有其他事务又修改了该数据。如果其他事务有更新的话,正在提交的事务会进行回滚;这样做不会有锁竞争更不会产生思索,但如果数据竞争的概率较高,效率也会受影响

MVCC – Multiversion concurrency control:

每一个执行操作的用户,看到的都是数据库特定时刻的的快照(snapshot), writer的任何未完成的修改都不会被其他的用户所看到;当对数据进行更新的时候并是不直接覆盖,而是先进行标记, 然后在其他地方添加新的数据,从而形成一个新版本, 此时再来读取的reader看到的就是最新的版本了。所以这种处理策略是维护了多个版本的数据的,但只有一个是最新的。

Key/Value

前文所述,leveldb中写入一条记录,仅仅是先写入binlog,然后写入memtable

  • binlog: binlog的写入只需要append,无需并发控制

  • memtable: memtable是使用Memory Barriers技术实现的无锁的skiplist

  • 更新: 真正写入memtable中参与skiplist排序的key其实是包含sequence number的,所以更新操作其实只是写入了一条新的k/v记录, 真正的更新由compact完成

  • 删除: 如前文提到,删除一条Key时,仅仅是将type标记为kTypeDeletion,写入(同上述写入逻辑)了一条新的记录,并没有真正删除,真正的删除也是由compact完成的

Sequence Number

  • sequence number 是一个由VersionSet直接持有的全局的编号,每次写入(注意批量写入时sequence number是相同的),就会递增

  • 根据我们之前对写入操作的分析,我们可以看到,当插入一条key的时候,实际参与排序,存储的是key和sequence number以及type组成的 InternalKey

  • 当我们进行Get操作时,我们只需要找到目标key,同时其sequence number <= specific sequence number

    • 普通的读取,sepcific sequence number == last sequence number
    • snapshot读取,sepcific sequenc number == snapshot sequence number

Snapshot

snapshot 其实就是一个sequence number,获取snapshot,即获取当前的last sequence number

例如:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
  string key = 'a';
  string value = 'b';
  leveldb::Status s = db->Put(leveldb::WriteOptions(), key, value);
  assert(s.ok())
  leveldb::ReadOptions options;
  options.snapshot = db->GetSnapshot();
  string value = 'c';
  leveldb::Status s = db->Put(leveldb::WriteOptions(), key, value);
  assert(s.ok())
  // ...
  // ...
  value.clear();
  s = db->Get(leveldb::ReadOptions(), key, &value);   // value == 'c'
  assert(s.ok())
  s = db->Get(options, key, &value);   // value == 'b'
  assert(s.ok())
  • 我们知道在sstable compact的时候,才会执行真正的删除或覆盖,而覆盖则是如果发现两条相同的记录 会丢弃旧的(sequence number较小)一条,但是这同时会破坏掉snapshot
  • 那么 key = ‘a’, value = ‘b’是如何避免compact时被丢弃掉的呢?
    • db在内存中记录了当前用户持有的所有snapshot
    • smallest snapshot = has snapshot ? oldest snapshot : last sequence number
    • 当进行compact时,如果发现两条相同的记录,只有当两条记录的sequence number都小于 smallest snapshot 时才丢弃掉其中sequence number较小的一条

Sstable

sstable级别的MVCC是利用Version和VersionEdit实现的:

  • 只有一个current version,持有了最新的sstable集合
  • VersionEdit代表了一次current version的更新, 新增了那些sstable,哪些sstable已经没用了等

Mainifest

每次current version 更新的数据(即新产生的VersionEdit)都写入mainifest文件,以便重启时recover

]]>
<![CDATA[leveldb源码笔记之sstable]]> 2016-08-22T11:19:00+08:00 http://1feng.github.io/2016/08/22/sstable-summary 整体看下sstable的组要组成,如下:

sstalbe 生成细节

sstable 生成时机:

minor compaction

immutable-memtable 中的key/value dump到磁盘,生成sstable

major compaction

sstable compact(level-n sstable(s)与level-n+1 sstables多路归并)生成level-n+1的sstable

首先是写入data block:

data block都写入完成后,接下来是meta block:

然后是data/meta block索引信息data/meta index block写入:

最后将index block的索引信息写入Footer

一个完整的sstable形成!

]]>
<![CDATA[leveldb源码笔记之写入操作]]> 2016-08-18T15:03:00+08:00 http://1feng.github.io/2016/08/18/leveldb-write 插入一条K/V记录

持有Writer的线程进入Writers队列,细节如下:

MakeRoomForWrite的流程图:

记录会首先写入磁盘上的binlog,避免程序crash时内存数据丢失:

此处我们做了一个极度夸张的假设来做演示:两条记录的大小超出一个block的大小, 以至于被一切为三

K/V记录插入内存中的Memtable:

]]>