├── README.md
├── doc
├── bigtable.md
├── chubby.md
├── consistent-hash.md
├── gfs.md
├── mapreduce.md
├── newsql.md
├── paxos-principle-first.md
├── raft_one.md
├── raft_three.md
├── raft_two.md
└── transaction-isolation-first.md
└── image
├── bigtable_data_model.png
├── bigtable_scan.png
├── bigtable_tablet_location.png
├── bigtable_tablet_serving.png
├── bigtable_write.png
├── chubby_session.png
├── chubby_structure.png
├── consistency_model.png
├── gfs_architecture.png
├── gfs_write_control.png
└── mapreduce_overview.png
/README.md:
--------------------------------------------------------------------------------
1 | # Introduction
2 | 此repo为本人读论文的总结。
3 |
4 | # Papers
5 |
6 | ## System
7 |
8 | - google file system ([学习笔记](doc/gfs.md))
9 | - bigtable([学习笔记](doc/bigtable.md))
10 | - mapreduce([学习笔记](doc/mapreduce.md))
11 | - chubby([学习笔记](doc/chubby.md))
12 | - spanner
13 | - dynamo
14 | - megastore
15 | - dremel
16 | - pregel
17 | - percolator
18 | - Sinfonia: A New Paradigm for Building Scalable Distributed Systems
19 | - google f1
20 | - google borg
21 | - Windows Azure Storage: A high available cloud storage service with strong consistency
22 | - facebook haystack
23 | - What's Really New in NewSQL? ([学习笔记](doc/newsql.md))
24 |
25 | ## Database
26 |
27 | ## Distrubuted Machine Learning System
28 |
29 | ## Protocol or Algorithm
30 |
31 | - Raft ([学习笔记一](doc/raft_one.md) | [学习笔记二](doc/raft_two.md) | [学习笔记三](doc/raft_three.md))
32 | - Paxos ([Basic Paxos](doc/paxos-principle-first.md))
33 | - Consistent Hash ([学习笔记](doc/consistent-hash.md))
34 | - A Critique of ANSI SQL Isolation Levels ([学习笔记](doc/transaction-isolation-fisrt))
35 | - CAP
36 |
--------------------------------------------------------------------------------
/doc/bigtable.md:
--------------------------------------------------------------------------------
1 | # 1. Introduction
2 |
3 | 本文为读bigtable的总结,分为以下部分:
4 |
5 | - Data Model
6 | - API
7 | - Building Blocks
8 | - Implementation
9 | - Refinements
10 |
11 | # 2. Data Model
12 |
13 | bigtable本质上是一个K/V存储,其中映射关系为:
14 |
15 | ```
16 | (row:string, column:string, time:int64)->string
17 | ```
18 |
19 | 如上所示,key为`row:string, column:string, time:int64`, value为`string`
20 |
21 | 以一个例子来说明该映射关系,如下图:
22 |
23 | 
24 |
25 | 如上图所示,表名为`WebTable`,以URL为row key,网页各方面属性为column name,其中网页内容存储在contents:列中,通过时间戳可以区分列的数据版本。
26 |
27 | ## 2.1 Rows
28 |
29 | Row key可以是任意的字符串,大小不超过64KB。每次针对单行的操作都是原子操作。
30 |
31 | Bigtable按照row key的字典序对数据排序,每张表按照row key的排序范围动态划分。每个划分的row范围称作是tablet,是数据分布和负载均衡的单元。客户端可以根据这个特性,来把需要一起读的数据尽可能的安排在一起。例如,对于`WebTable`,如果需要同一个域名下的网页尽量放在一起的话,可以把域名`maps.google.com/index.html`按照row key`com.google.maps/index.html`来存储。
32 |
33 | ## 2.2 Column Families
34 |
35 | Column Key按照column family分组存储,访问控制信息也是按照column family为单元来设置的。一般来讲,期望的是所有存储在column family的列的数据类型是一样的,因为bigtable是按照column family进行压缩的。
36 |
37 | 在进行列存储前,column family必须要先创建,一旦创建好,任何在此family下的column key都可以使用。bigtable期望的是一张表的column family的schema几乎是不变的,且一张表的column family数量较小(最多几百个),但是,列的数量是无限制的。
38 |
39 | Column key的表现形式为`family:qualifier`,对于family必须是可打印的字符串,对于qualifier可以是任意的字符串。以column faimily`anchor`为例,每个column key都代表一个anchor,例如`anchor:cnnsi.com`。
40 |
41 | ## 2.3 Timestamp
42 |
43 | Bigtable会对每个单元格的数据存储多个版本,版本按照时间戳排序。Bigtable timestamp是64位整数,可以由bigtable内部生成或者client端指定。
44 |
45 | 为了方便client指定需要存储多少个版本的数据,bigtable可以针对每个column family来设置,例如可以设定只存储最新的n个版本或者最近n天的数据。
46 |
47 | 对于`WebTable`,timestamp为爬取网页内容的时间戳,设定为只存储最新三个版本的数据。
48 |
49 | # 3. API
50 |
51 | Bigtable API提供了创建和删除表、column family的API,还提供了设置集群、表、column family元数据的API,例如改变访问控制权限。
52 |
53 | **写和删除的例子**
54 |
55 | 
56 |
57 | **遍历数据的例子**
58 |
59 | 
60 |
61 | ## 4. Building Blocks
62 |
63 | Bigtable使用了其他的一些系统来构建服务:
64 |
65 | - 使用了GFS来存储日志和数据文件
66 | - 集群管理系统来负责分配管理资源,监控机器状态等
67 |
68 | ## 4.1 SSTable
69 |
70 | Bigtable底层使用SSTable文件格式来存储数据。一个SSTable是存储多个K/V数据,数据按照KEY排序并且不能修改。对SSTable可以执行的操作包括,按照指定Key查找,按照Key Range遍历。
71 |
72 | 在SSTable内部,包含一些列的block(一般每个block是64KB,但也是可以配置的),在SSTable的尾部存储了block的索引,方便快速查找Key在哪个block内。当SSTable文件打开时,是会把block的索引加载到内存中去的。对于一次查找一般会有一次磁盘开销,先从内存中定位到在哪个block,然后把block从磁盘中读出来。
73 |
74 | ## 4.2 Chubby
75 |
76 | Bigtable里面另一个依赖比较重的服务是Chubby。Chubby是一个分布式的锁服务,一般五副本冗余存储,其中一个会被选为主,只要五副本中的大多数是正常状态,chubby就可以保持可用。
77 |
78 | Chubby提供一个包含目录或文件的命名空间,每个目录或者文件都可以当作是锁服务来使用,对单个文件的读或者写都是原子的。Chubby Client Library提供Chubby文件的缓存,每个Chubby Client和Chubby服务端通过一个session保持链接。一个Client的Session会在其无法在lease过期时间内续约而到期。当一个Client的session到期后,它会丢失所有的锁和handle。Chubby client可以在文件或者目录下注册回调事件,当文件或目录发生变化是会通知相应的client。
79 |
80 | 在Bigtable中,使用到Chubby的地方如下:
81 |
82 | - 发现tablet server和tablet server挂掉
83 | - 存储bigtable schema信息,主要是column family信息,以及存储访问控制信息
84 |
85 | 备注:如果chubby挂掉或者chubby和bigtable之间通信断掉,那么bigtable服务将不可用。
86 |
87 | ## 5. Implementation
88 |
89 | Bigtable中有三大组件,包括client,单master和许多的tablet server。可以根据负载的情况,动态的加入或者删除机器。
90 |
91 | **master的工作**
92 |
93 | - 分配tablet到tablet server
94 | - 检测tablet server的加入和过期
95 | - 均衡tablet server的负载
96 | - garbage collection GFS中的文件
97 | - schema变更,包括表和column family的创建等
98 |
99 | 由于master不存储tablet location信息,因此,client基本上从来不会与master通信,master不会成为系统的瓶颈。
100 |
101 | **tablet server的工作**
102 |
103 | - 管理一组tablet
104 | - 处理tablet的读写请求
105 | - 分裂过大的tablet
106 |
107 | 每个Bigtable集群存储多张表,每张表包含多个tablet。初始的时候,一张表只有一个tablet,当表数据量增加时,会自动分裂成多个tablet。
108 |
109 | ## 5.1 Tablet Location
110 |
111 | tablet location采用三层的存储层次,如下图:
112 |
113 | 
114 |
115 | 1. Chubby file存储Root tablet的location
116 | 2. Root tablet存储othter MetaData tablet的location,root tablet不能分裂,需要保证三层结构
117 | 3. 每个Metadata tablet存储一组user tablet的location
118 |
119 | `Metadata tablet`的每行存储user tablet的row range的开始行和结束行等信息。
120 |
121 | 对于Client端,会缓存tablet location。如果客户端发现它的tablet location为空或者不正确,会从最上层开始查找和比较。对于为空的情况,需要三次网络开销来读取信息;对于不正确的情况,最坏需要六次网络开销,因为可能对于三层结构都出错了,每次都要试一次错,再读一次正确的数据。为了提升性能,client端一次会预取多个tablet的location并缓存。
122 |
123 | tablet location信息应该都是存储在tablet server上的。
124 |
125 | ## 5.2 Tablet Assignment
126 |
127 | master负责跟踪活着的tablet server和tablet到tablet server的分配,包括哪些tablet没有分配。当一个tablet没有被分配,并且一个tablet server可以容纳下该tablet,master给该tablet server发送请求告诉其load该tablet。
128 |
129 | 问题:这里的sufficient room指的是什么指标?
130 |
131 | **Chubby用来跟踪tablet server状态**
132 |
133 | - 当一个tablet server启动的时候,它会在Chubby指定目录创建一个文件,并获取一把锁
134 | - master通过监控目录来发现新加入的tablet server
135 | - tablet server在锁丢失时停止服务
136 | - 当其所创建的文件还存在的情况下,tablet server会尝试重新获取锁
137 | - 如果其创键的文件不存在,tablet server会停止进程
138 | - 当tablet server终止时,会释放锁,然后master可以跟快速的来重新分配tablet
139 |
140 | **master的工作**
141 |
142 | master的工作包括两方面
143 |
144 | - 检测停止服务的tablet server
145 | - 尽快的重新分配tablet到tablet server
146 |
147 | master周期性和tablet server通信来询问其锁的状态,如果是
148 |
149 | - tablet server汇报其已经丢失了锁
150 | - master多次重试无法和tablet server通信,接着master尝试和chubby通信,获取该server的文件所,根据结果分为两种情况:第一是master可以获得锁,那么说明tablet server挂掉或者其与Chubby无法通信,那么,master可以确信它无法提供服务了,因此立即删除Chubby上的文件,然后,把这台tablet server上的tablet都设置成未分配状态;第二是master无法获得锁,说明master和Chubby通信有问题,master会停止进程。
151 |
152 | 问题:master重新分配tablet的时机是?
153 | 问题:为什么master不直接和tablet server维持租约?
154 | 问题:为什么master不直接看chubby上的锁状态来确定tablet server的状态?有可能tablet server和chubby可以连接,但master和tablet server连不上。
155 |
156 | master启动时,需要先检测当前的tablet分配情况,然后才能改变分配情况,启动步骤如下
157 |
158 | 1. 从Chubby获取唯一的`master`的锁,防止出现多个master
159 | 2. master扫描Chubby指定目录,发现活的tablet server
160 | 3. master和活的tablet server通信,来获取它们已经分配了哪些tablet
161 | 4. master扫描metadata table,去获取所有的tablet,当扫描发现有没有分配的tablet,把它加入到未分配的tablet中
162 |
163 | 其中有个问题是扫描metadata table之前,需要metadata的tablets都被分配了。因此,在扫描之前,需要先把root tablet加入到未分配的tablet中,这样root table在后面就会被分配了。因为root tablet中,有所有的metadata的tablets的信息,然后可以通过这些信息来确定哪些metadata的tablet未分配,然后把它们分配到相应的tablet server。
164 |
165 | **tablet发生改变的时机**
166 |
167 | - tablet创建或删除
168 | - tablet分裂,或者tablet合并
169 |
170 | **tablet分裂**
171 |
172 | tablet分裂是由tablet server发起的,tablet最终会把分裂信息写入到metadata table中,然后通知master。为了防止通知丢失,当master让一个tablet server去load刚刚分裂的tablet时,master检测到新的tablet。tablet server会告诉master分裂的情况,因为,tablet会发现master要求load的tablet是几个tablet的和。
173 |
174 | master让tablet server去load tablet的时机一般不是启动的时候?还有就是检测到没有分配的时候?
175 |
176 | 目前猜测master会有后台任务定期的扫描未分配的tablet。
177 |
178 | ## 5.3 Tablet Serving
179 |
180 | tablet的存储最终持久化到GFS,其中包括SSTable数据和commit log。最近的commit log会在内存中存储,并持久化tablet log中,之前的commit log可能会dump到GFS中,以SSTable来存储。
181 |
182 | 
183 |
184 | **tablet recovery**
185 |
186 | 读取最近一次dump到SSTable中的commit log,然后再此之上,把之后的commit log的操作replay内存中。
187 |
188 | **write**
189 |
190 | 写操作首先会检查语法是否出错以及是否有权限写,检查权限通过一个chubby file来获取(通常在client端有cache)。合法的操作就被持久化到commit log中,然后插入到memtable里面。为了优化性能,会采用group commit机制来组合提交多次写操作的log。
191 |
192 | **read**
193 |
194 | 读操作首先会检查语法是否出错以及是否有权限读。最终读出的数据是一些列的SSTable和Memtable合并后的数据,因为它们都是按照key排序的,合并效率是比较高的。
195 |
196 | 读写操作可以在tablet合并或者分裂的时候进行。
197 |
198 | ## 5.4 Compactions
199 |
200 | bigtable中compaction分为minor compaction,merging compaction和major compaction。
201 |
202 | **minor compaction**
203 |
204 | 当memtable达到一定大小限制时,冻结的memtable会被转换成SSTable,存储到GFS中。
205 |
206 | minor compaction的目标主要有
207 |
208 | - 减少tablet server的内存使用
209 | - 减少tablet recovery的时间
210 |
211 | **merging compaction**
212 |
213 | 每次minor compaction都会生成一个新的SSTable。如果minor compaction较多,会生成一批SSTable,每次读操作都会需要合并这些SSTable的数据。为了提升读性能,bigtable限制这些文件的个数,会定时的执行merging compaction,来合并部分SSTable和memtable,然后生成一个新的SSTable。
214 |
215 | **major compaction**
216 |
217 | major compaction将所有的SSTable合并成一个SSTable。major和非major的区别在于,major compaction后的SSTable不包含任何的已经删除的数据,而非major compaction有可能会包含。
218 |
219 | # 6. Refinements
220 |
221 | 本部分讨论一些提升系统性能、可用性和可靠性的方法和技术。
222 |
223 | ## 6.1 Locality groups
224 |
225 | client可以指定多个column family到一个locality group,每个locality group会单独指定一个SSTable,这样读单个locality group的数据会更高效。例如,在`WebTable`中page meta放到一个locality group,而`content`可以放到另一个locality group。这样读page meta的时候,就不需要去读page content信息了。备注:可能的坏处时,如果需要读多个locality group的数据的时候,这样就会比较低效,需要应用控制好locality group的划分,适合业务特征。
226 |
227 | 另外,locality group中的SSTable可以配置放到内存中,采用延迟加载的方法,一旦放入内存后,下次读SSTable的时候就可以从内存中查找数据了。例如,metadata table的数据被配置成这种方式。
228 |
229 | ## 6.2 Compression
230 |
231 | 按照locality group来进行压缩,client可以配置具体的压缩算法。具体地压缩的时候,对每个SSTable的block来进行压缩的,这样的好处是,读SSTable的数据的时候,不需要解压整个文件。在google应用中,需要client采用两级压缩的方法,先采用`Bentley and McIlroy`方法压缩长公共字符串,接着采用一个快速压缩算法,按照16KB为块来进行压缩。在`WebTable`中把同一个host的内容放在一起,可以提升压缩比例,并且,row key的取名也比较重要。
232 |
233 | ## 6.3 Caching for read performance
234 |
235 | 采用两级cache,其中**Scan Cache**用来存储访问过的K/V对,对重复读某些数据的性能的应用比较友好;**Block Cache**缓存从GFS中读取的SSTable block,对读附近的数据的应用比较友好。
236 |
237 | ## 6.4 Bloom filters
238 |
239 | 在bigtable中,经常需要从多个SSTable中合并出数据,返回给client。如果SSTable不在内存中,将会有比较多的磁盘I/O。Bloom filter可以帮助过滤掉大部分那些SSTable中没有某行数据的,以减少访问SSTable的次数。
240 |
241 | ## 6.5 Commit-log implementation
242 |
243 | commit log可以有两种实现方式,第一种是每个tablet一个log文件,第二种是一个tablet server一个log文件。
244 |
245 | 如果采用第一种方式实现,每个写操作都要去写对应的文件,需要大量的disk seek去写到不同的物理文件中。并且,对于group commit的优化会减弱,只有同一个tablet的才会形成一个group。
246 |
247 | 采用第二种方式实现会改善上面两种操作的效率,但是对于tablet recovery不友好。当一个tablet server宕机后,其上的tablet会被多个不同的其他tablet server加载上。这些tablet server只需要该tablet server的某些tablet的commit log,而不是需要所有的,解决方案是,先把commit log按照
排序,然后,每个tablet server读取其对应需要commit log。为了优化排序性能,采用多路排序的方法,master把日志文件分成多个64MB的文件,放到不同的tablet server排序。
248 |
249 | 写日志的时候,有可能会碰到GFS的写操作延迟比较大,例如碰到要写入的副本宕机等情况,bigtable采用的解决方案是,用两个线程来写日志,每个写入自己的文件。同一时刻,只有一个线程在工作,如果当前线程的写入性能比较差,那么切换到另一个进程。
250 |
251 | 问题:最终的commit log应该是两个线程写入的文件内容去重后合并的结果?
252 |
253 | ## 6.6 Speeding up tablet recovery
254 |
255 | 把一个tablet从某个tablet server A迁移到另一个tablet server B的流程
256 |
257 | 1. A上对该tablet做一次minor compaction,以减少replay log的时间
258 | 2. A对该tablet停止服务,需要再做一次minor compaction,因为从上次做minor compaction到停止服务期间可能有新的写入操作
259 | 3. 在B上一次加载这两次minor compaction的内容到内存中
260 |
261 | 问题:中间停止服务的时间应该是从步骤二开始到步骤三结束,大约时间需要多少?
262 |
263 | ## 6.7 Exploiting immutability
264 |
265 | 对于SSTable,由于SSTable是不可修改的,因此,对于SSTable文件的访问可以并行,无需要进行同步。对于分裂操作,也可以让分裂后的tablet共享原来的SSTable。
266 |
267 | 对于memtable,采用的是copy on write机制,保证读和写可以并行。即先拷贝一份数据出来,修改后,再写入到memtable中。
268 |
269 | 对于已删除的数据,是放到SSTable文件的清理来做的,master采用`mark-and-sweep`机制来清理。
270 |
271 |
272 |
273 |
--------------------------------------------------------------------------------
/doc/chubby.md:
--------------------------------------------------------------------------------
1 | # 1. Introduction
2 |
3 | 本文是读google chubby的论文总结。
4 |
5 | chubby是一个分布式的锁服务,其设计目标包括两点:
6 |
7 | - 使用清晰易懂的语义,为大规模数量的客户端提供服务,并且保证可靠性和可用性
8 | - 吞吐量和存储能力的优先级放在第二位
9 |
10 | chubby的使用场景有很多,例如
11 |
12 | - GFS用chubby来选举master
13 | - Bigtable用chubby来选举master,发现tablet server等等
14 |
15 | 对于选择问题,是经典的分布式一致性问题,chubby采用Paxos算法来保证一致性。
16 |
17 | # 2. Design
18 |
19 | ## 2.1 Rationale
20 |
21 | 本节讨论了google为什么以服务的方式来实现chubby,而不是采用Client Paxos Library方式来实现chubby的原因,包括
22 |
23 | - 一个应用刚起步的时候负载往往比较小,所以对于可用性的需求比较少,当应用发展到一定规模时,就会在可用性上花费功夫,如果采用Client Paxos Library的方式,对于应用的改动量比较大;而如果采用服务的方式,则需要简单的几条语句就能实现。因此,采用服务方式,是符合应用增长的趋势的需求的。
24 | - 许多客户端使用chubby来进行选主,因此,需要机制来广播结果。chubby通过小文件的读写很方便的实现了这个功能。这个功能也可以用命名服务来实现,但集成到chubby中可以减少客户端依赖的server数量,并且命名服务和锁服务用到的分布式一致性的协议是一样的,所以,没有必要重复造轮子。
25 | - 开发者对于基于锁的接口更熟悉,更容易上手。
26 | - 分布式一致性算法为了保证可用性,需要多副本才能保证高可用性,采用服务的方式,单个客户端也能保证锁服务的可用性。
27 |
28 | 基于上述的比较,chubby的两个关键设计目标为
29 |
30 | - 提供锁服务,而不是client library
31 | - 采用小文件来广播选主的结果,而不是维护另一个服务
32 |
33 | 基于使用环境的反馈,chubby还有如下设计目标
34 |
35 | - 通过chubby文件广播选主结果的服务可能有成千上万个客户端,因此,需要允许这些客户端来监听此文件的变化,最好不需要投入太多的服务器来支撑。
36 | - 在主发生变化的时候,客户端需要及时知道,因此,提供事件通知机制比客户端轮询要更有效。
37 | - 有不少客户端有轮询文件的需求,因此,实现文件缓存是非常有必要的。
38 | - 实现一致性缓存,方便开发者使用。
39 | - 为了安全性,提供访问控制功能。
40 |
41 | 最重要的一点是,chubby期望开发者使用的是粗粒度的锁服务。
42 |
43 | 粗粒度的锁服务和细粒度的锁服务的区别如下
44 |
45 | - 粗粒度锁一般是很少被获取,一旦有客户端获取锁,占用的时间会比较长,经常以小时或者天为单位;细粒度锁被占用的时间比较短,但是,会被客户端频繁获取。
46 | - 粗粒度锁服务的获取频率和业务的执行频率无关;客户端对粗粒度锁服务的恢复开销比较大,因此,需要在服务端支持高可用性,即锁所在服务器挂掉不会造成客户端失去锁的控制权;临时的锁服务不可用不会造成太多客户端的故障。
47 | - 细粒度锁服务的获取频率和业务的执行频率相关,客户端对细粒度锁服务的恢复开销不大,因此,锁在的服务器挂掉后,一般不恢复客户端获取的锁;临时的锁服务不可用会造成很多客户端故障。
48 |
49 | ## 2.2 System structure
50 |
51 | chubby的两个关键组件是chubby library和chubby servers(chubby cell),另一个可选的组件是proxy server,将在3.1节讨论。
52 |
53 | chubby cell由多台chubby server组成,也称作副本,采用分布式一致性协议选主。副本都会维护数据库的拷贝,但只有主提供读和写服务,其他副本只从主拉取最新的数据。
54 |
55 | 
56 |
57 | **客户端发现主的流程**
58 |
59 | 所有副本的IP信息放在DNS中,客户端通过和DNS列出的服务器通信来获取master地址。如果客户端请求的是一台非master服务器,它会返回master的地址;如果请求的master,则会告知客户端。客户端定位master后,会把所有请求都提交给master,直到它停止响应或者不再是主了。
60 |
61 | **读写请求**
62 |
63 | 写请求通过一致性协议,需要写入多数派的副本才算写成功。
64 |
65 | 读请求只能通过master来响应,如果master挂掉,会在秒级别选出新主提供服务。
66 |
67 | **副本故障**
68 |
69 | 如果某个副本挂掉,并且小时级别内没有恢复,那么采用以下故障恢复流程
70 |
71 | - 从资源池中选择一台新机器,在上面安装chubby server,并启动服务。
72 | - 更新DNS表,用新机器的IP地址替换老的。
73 | - 当前的master会定期询问DNS,然后发现新替换的server,然后把新成员更新到数据库里面,这个更新会通过一致性协议同步到其他的副本上。
74 | - 新替换的chubby server也会从master拉取最新的数据库拷贝,可能会基于最新的备份,然后恢复其之后的修改。
75 |
76 | ## 2.3 Files, directories, and handles
77 |
78 | chubby提供一个类似于UNIX的文件系统接口,一个典型的名称为
79 |
80 | `ls/foo/wombat/pouch`
81 |
82 | - `ls`前缀对于所有chubby名称都一样,代表`lock service`的意思
83 | - `foo`是一个chubby cell的名称,DNS中会根据这个名称查找到一台或多台chubby server
84 | - `/wombat/pouch`是在chubby cell里面的路径名
85 |
86 | chubby的文件系统接口和UNIX不同的地方有
87 |
88 | - 为了让不同目录的文件可以由不同的chubby master来提供服务,chubby不允许文件从一个目录移动到另一个目录
89 | - 不维护目录的修改时间
90 | - 文件权限由文件本身的设置有关,跟其目录的权限无关
91 | - 为了更容易缓存文件元数据,系统不暴露最后访问时间给客户端
92 |
93 | 命名空间中只包含文件和目录,统称为节点(node),节点的特点如下
94 |
95 | - 每个节点没有软连接或者硬连接
96 | - 节点可以是永久的或者临时的。临时节点在客户端和chubby server连接断掉之后会自动删除,可以用来标识客户端是否可用
97 | - 每个节点都可以加读/写锁
98 |
99 | 节点的元数据信息包括控制读的ACL文件名,控制写ACL文件名和改变ACL名称的文件名。除非特别指定,节点在创建的时候会继承父目录的名称。ACL本身也是文件,存储在chubby的ACL目录,文件中存储的是拥有此文件此权限的用户名。例如,一个文件F的写ACL文件名为foo,foo文件中包含一条记录bar,那么用户bar拥有文件F的写权限。
100 |
101 | 除了ACL相关信息,元数据还包括
102 |
103 | - instance number:todo
104 | - content generation number(文件特有),当文件被写入时,会增加
105 | - lock generation number,当锁从空闲到被持有,会增加
106 | - ACL generation number,当节点的ACL文件被写入时,会增加
107 |
108 | chubby对客户端会暴露一个64位的文件checksum,方便比较文件是否发生改变。
109 |
110 | chubby会为每个打开文件的客户端分配一个handle,其包含的信息有:
111 |
112 | - 检查位,用于权限控制
113 | - 序列号,用于标记此handle是否由之前的master生成的
114 | - 打开模式,用于master切换时能够恢复
115 |
116 | ## 2.4 Locks and sequencers
117 |
118 | 每个chubby文件和目录都能获取读和写锁,对于没有获得锁的也能访问/读写目录和文件,这和mutex是一致的。
119 |
120 | 分布式锁可能会导致由于通信原因导致不一致性,例如
121 |
122 | - 客户端1获取锁L,然后发一个请求R,接着挂掉
123 | - 在R还没有达到chubby server之前,另一个客户端2获取锁L,然后做一些操作
124 | - R在经过漫长的网络拥塞后,到了chubby server,这时候,如果响应R的操作请求,可能会导致数据的不一致
125 |
126 | chubby采用lock sequencer方式来解决上述问题,具体地如下
127 |
128 | - 客户端获取锁之后,马上向chubby server获取sequencer,里面包含锁名称,模式(读/写)以及lock generation number,当客户端的某些操作需要锁来保护时,它需要把sequencer信息也传递给chubby server,这样chubby server会判断sequencer是否还合法,如果不合法,chubby server会拒绝客户端的操作请求。
129 |
130 | 虽然采用lock sequencer的方式才有效解决问题,但是其在性能上比较差,chubby提供了一种不完美但是减轻由于通信导致的不一致性问题,具体如下
131 |
132 | - 对于正常释放的锁,马上可以被其他客户端占用;对于非正常释放的锁,设置一个lock-delay时间,在这个时间段内,其他客户端不能获取锁,目前lock-delay时间设置为1分钟。
133 |
134 | ## 2.5 Events
135 |
136 | 客户端可以在创建handle的时候订阅一些列的事件,在chubby中,可订阅的事件包括
137 |
138 | - 文件内容变化:通常用于广播某个服务的地址
139 | - 子节点的新增、删除或修改:todo
140 | - chubby master挂掉:告诉客户端其他事件可能丢失,需要重新扫描
141 | - handle非法:通常说明通信出问题
142 | - 获得锁:可以用来确定主是否被选出来了
143 | - 来自其他客户端的锁冲突请求:允许锁的cache(不太明白)
144 |
145 | 根据google的经验,后两种事件很少使用到。
146 |
147 | ## 2.6 API
148 |
149 | 客户端通过`open`函数获得handle,通过`close`销毁handle。首先来看`open`操作的使用方式
150 |
151 | - 通过节点名称来操作,并且可以提供一系列的选项,包括ACL信息,需要监听的事件,lock-delay等等
152 |
153 | `close`操作会关闭一个handle,后续所有对该handle的操作都是不允许的。
154 |
155 | 剩下的其他操作都是基于handle来使用的,包括
156 |
157 | - `GetContentsAndStat`返回文件内容和元数据信息
158 | - `SetContents`写入内容到文件
159 | - `Delete`如果某个节点没有孩子节点的话,删除它
160 | - `Acquire`,`TryAcquire`和`Release`获得和释放锁
161 | - `GetSequencer`返回锁的sequencer
162 | - `SetSequencer`:设置锁的sequencer
163 | - `CheckSequencer:`检查锁的sequencer是否合法
164 |
165 | ## 2.7 Caching
166 |
167 | 为了减少网络流量,客户端会缓存文件内容和元数据信息在一致性的,写直达的内存cache中。cache通过lease机制来维护,通过master来发送invalidation来保持一致性,保证客户端可以看到一致性的chubby state或者报错。
168 |
169 | **文件和元数据**
170 |
171 | 当文件内容或者元数据发生变化时,修改会阻塞直到master发送给所有缓存了这部分数据的客户端,告知它们废弃此cache。这个机制是放在心跳RPC中实现的,当客户端接收到作废cache的请求时,客户端会写入cache作废的状态,然后再下次心跳的时候返回给master确认消息。修改操作直到master知道所有的客户端都作废了其cache或者让cache过期。
172 |
173 | 问题:如果这个时候有客户端连不上了,应该怎么处理?
174 |
175 | 在为收到所有确认作废的消息之前,master会设定此节点是不可放到cache中的,这样不会阻塞读,所有的读请求都会直接到master请求数据。
176 |
177 | cache的协议非常简单,当数据发生变化时,使它作废,但从来不更新。发生变化的时候不更新是因为更新效率低,不如需要的时候,再由客户端重新去master拿数据。
178 |
179 | **handle**
180 |
181 | chubby也会缓存handle,所以,一旦某个文件在客户端打开过,再次打开是可以直接走cache,而不用通过RPC的。
182 |
183 | **lock**
184 |
185 | chubby允许缓存lock,使得客户端占用锁的时间比实际使用时间长。如果有其他客户端需要使用这把锁,可以通过监听锁冲突事件,来及时的释放锁。
186 |
187 | ## 2.8 Session and Keep-Alives
188 |
189 | session是客户端和chubby server之间的通信机制,通过KeepAlive消息来维持。客户端的handle、锁等在session是合法的情况下,才能保持合法状态。
190 |
191 | 客户端在首次和master通信的时候建立session,客户端只有在它退出或者session在一分钟内其上没有open的handle和请求时,才会主动地关闭session。
192 |
193 | 每个session都有lease,lease都有timeout。收到KeepAlive消息后,master通常会阻塞此消息的回复,直到客户端快过期的时候,再返回Keep-Alive消息,告知客户端新的timeout,然后,客户端又会立马发一个Keep-Alive消息,因此,服务端通常都有一个Keep-Alive阻塞。当服务端过载的时候,可能会调大timeout的值。
194 |
195 | Keep-Alive还可以用来传递事件和cache invalidation消息,这个时候Keep-Alive是可以不阻塞,在更早的时间返回给客户端的。
196 |
197 | 客户端本地也维护了一个lease timeout时间,它的timeout与服务端的不同,考虑了Keep-Alive消息的传输时间以及服务端和客户端时钟的不一致。为了保持一致性,要求服务端的时钟不会比客户端的时钟块一个已知的常量级别。
198 |
199 | 当客户端的lease timeout时间到了,它不确定服务端是否关闭了此session。于是,它先把cache清空并禁用,此时session的状态称作为`jeopardy`。客户端会再等待45秒,称作是`grace period`,当客户端在45秒内,收到了master的Keep-Alive消息,那么,客户端会把cache启用,否则,客户端认为session过期了。
200 |
201 | 
202 |
203 | 当session变成`jeopardy`状态,客户端会给应用推送`jeopardy`事件;如果后面session变成正常了,会推送给应用`safe`事件;当session过期了,会推送`expired`事件,方便应用来处理各种情况。
204 |
205 | ## 2.9 Fail-overs
206 |
207 | 当master挂掉之后,其内存中关于session,handle,lock的状态都会丢掉,因此,lease timeout计时器也会停止计时,这在maste挂掉情况下是合理的。如果重新选主时间比较短,那么可以在下次客户端session timeout之前完成,那么会一切正常;如果选主时间比较长,但是在`grace period`之内,客户端会废弃cache内容并禁用cache,等待重新和master连接,如果在`grace period`之内能连上master,那么可以启用cache。
208 |
209 | Master的fail-over过程如图2所示,具体流程为
210 |
211 | 1. old master和客户端之间有session M1
212 | 2. 客户端和old master通过Keep-Alive消息2,把心跳timeout扩展到lease C2
213 | 3. 在lease C2期间,old master挂掉
214 | 4. lease C2到期,客户端进入`grace period`,等待master的lease
215 | 5. new master选举出来,收到客户端发的Keep-Alive消息4,new master会拒绝这个消息,因为它使用的是old master的epoch number
216 | 6. 然后客户端重新给new master发Keep-Alive消息6,然后,把客户端的租约扩展到了lease C3
217 |
218 | **new master恢复和客户端的session状态的流程**
219 |
220 | 为了让应用层对master fail-over无感知,客户端和master一起来协作来构建内存中session的状态。主要通过三部分来完成
221 |
222 | - 通过持久化的database,其中包含了session,锁持有以及临时文件的信息
223 | - 通过从客户端获取状态
224 | - 通过保守的估计
225 |
226 | 具体的流程为
227 |
228 | 1. 选择新的epoch number,当有客户端用old master的epoch number来连接的时候会拒绝
229 | 2. master可能会响应获取master location的请求
230 | 3. 从数据库中读取session和锁的信息,然后构建到内存中,seesion的lease将会被扩展到之前的master用到的最大值
231 | 4. master允许客户端发送Keep-Alive消息,但是不允许其他session相关的操作
232 | 5. 发送failover事件给客户端,让客户端清空cache,告知应用可能丢失了事件
233 | 6. master等待每个session确认fail-over事件或者让session过期
234 | 7. master允许所有的操作
235 | 8. 如果客户端使用fail-over之前的handle,master会在内存中重新构建;如果重新创建的handle是关闭的,那么master会在内存中记录,表明它不能在这轮master epoch中重新创建。
236 | 9. 过一段时间,例如,一分钟,master删除没有open handle的临时文件
237 |
238 | ## 2.10 Database Implementation
239 |
240 | 一开始采用Berkeley DB,支持日志的一致性同步,但由于维护比较复杂,后来自己开发简易版的。
241 |
242 | ## 2.11 Backup
243 |
244 | 每隔几个小时,chubby cell把数据库快照写入到GFS中。
245 |
246 | ## 2.12 Mirroring
247 |
248 | Chubby允许文件集合从一个chubby cell镜像到另一个chubby cell。镜像操作往往很高效,原因有
249 |
250 | - chubby file通常很小
251 | - 事件通知机制可以很快让镜像代码在目的chubby cell做相同的操作
252 |
253 | 如果某个镜像断网了,当网络重新连接时,通过比较checksum来确定期间更新的文件。
254 |
255 | 通常使用镜像机制来把配置文件拷贝到多个集群。一个特别的chubby cell,global,包含`/ls/global/master`会把这个子树拷贝到其他chubby cell的`ls/cell/slave`。global chubby cell的五个副本放在全球五个不同的地方,所以,它的可用性是非常高的。
256 |
257 | # 3. Mechanisms for scaling
258 |
259 | todo
260 |
261 | # 4. Use, surprises and design errors
262 |
263 | todo
--------------------------------------------------------------------------------
/doc/consistent-hash.md:
--------------------------------------------------------------------------------
1 | # 引言
2 |
3 | 在分布式环境中,由于数据量庞大,往往需要对数据做分区,分区有两种:一种是range分区,另一种是hash分区。顾名思义,hash分区是采用hash算法,将数据划分到不同的分区中,采用传统的hash算法能有效地将数据划分到不同的分区,但是,传统的hash算法在机器上下线时,由于hash映射的变化,会导致大量的数据发生迁移。本文以分布式缓存为场景,分析了传统hash算法的缺点,并讨论了consistent hash如何解决该问题,以及consistent hash的优化和实现。
4 |
5 | 本文的按如下组织:
6 |
7 | - 传统hash算法的缺点
8 | - consistent hash
9 |
10 | 本文收录在我的github中[papers项目](https://github.com/Charles0429/papers),papers项目旨在学习和总结分布式系统相关的论文。
11 |
12 | # 传统hash算法的缺点
13 |
14 | 以分布式缓存场景为例,如下:
15 |
16 | 
17 |
18 | 对于传统hash分区方法,针对某个(key,value)对,其存储的缓存节点下标可以用hash(key) % N,在正常情况下,能良好地工作。但是,当机器宕机宕机下线时,可能会涉及到大量的数据的迁移,因为机器数量减少为N-1,对应的hash取模后,大量的(key,value)对到Cache Server的映射发生改变,导致大量的(key,value)数据在Cache Server间迁移,以一个例子说明这个问题:
19 |
20 | 假设hash函数为y = x + 1,系统中有如下(key,value)对,机器数量为5台
21 |
22 | ```
23 | (0,1)
24 | (1,2)
25 | (2,3)
26 | (3,4)
27 | (4,5)
28 | ```
29 |
30 | 根据hash函数取模,得到(key,value)到Cache Server的如下映射:
31 |
32 | ```
33 | (0,1) -> Cache Server 1
34 | (1,2) -> Cache Server 2
35 | (2,3) -> Cache Server 3
36 | (3,4) -> Cache Server 4
37 | (4,5) -> Cache Server 5
38 | ```
39 |
40 | 假设其中Cache Server 1宕机,(key, value)到Cache Server重新映射如下:
41 |
42 | ```
43 | (0,1) -> Cache Server 2
44 | (1,2) -> Cache Server 3
45 | (2,3) -> Cache Server 4
46 | (3,4) -> Cache Server 2
47 | (4,5) -> Cache Server 4
48 | ```
49 |
50 | 可以看出,上述极端例子中,所有的(key,value)对都发生了迁移,这个开销是相当大的。在分布式系统中,由于机器数量众多,机器发生故障是常态,因此,采用传统的hash算法是不合适的。
51 |
52 | # consistent hash
53 |
54 | 为了解决上述问题,引入了consistent hash算法,如下图:
55 |
56 | 
57 |
58 | 在consistent hash中,有一个hash环,代表一个范围,例如,可以取值为[0,2^64-1]。对于,每个Cache Server会根据hash函数算出一个整数值,最终落到hash环的某个点上,如图中的Cache Server 1-5。每个(key,value)对存储在hash环上顺时针的下一个Cache Server,举个例子,假设hash(Cache Server i) = Hi,i=1..5,如果(1,2)的hash取值处于[H1,H2)之间的话,那么它会存储在Cache Server2上,以此类推。
59 |
60 | 因此,consistent hash能很好地应对Cache Server宕机情况,假设还是Cache Server 1宕机,如下图:
61 |
62 | 
63 |
64 | 上图中Cache Server 1发生宕机的话,整个系统中只有(0,1)会从宕机的Cache Server 1迁移到Cache Server 2,比传统的hash算法会好很多。
65 |
66 | 但上述的consistent hash也有其缺点:
67 |
68 | - Cache Server在hash环上的分布可能不均匀,导致Cache Server间的负载不均衡
69 | - Cache Server的配置可能不同,所以能承受的负载也不同,而上述的consistent hash是没有考虑这个因素的
70 |
71 | 为了说明上述问题,我实现了上述的consistent hash算法,并且模拟了10000个key,10台机器的场景下各个server的负载情况如下
72 |
73 | ```
74 | Node1:324
75 | Node6:689
76 | Node3:75
77 | Node2:217
78 | Node5:28
79 | Node8:865
80 | Node0:657
81 | Node9:3157
82 | Node4:2284
83 | Node7:1704
84 | ```
85 |
86 | 可以看出机器之间的负载不均衡程度很高,为此,引入了虚拟节点的概念。一个实际的Server可以拆分成多个虚拟节点,虚拟节点根据hash值会散落在hash环的不同地方,这样,在虚拟节点个数较大时,负载就会趋向于均衡。
87 |
88 | 下面做了一组实验,模拟了虚拟节点的数量从1,10,50,100,200,400,800的时候,各个Cache Server的负载均衡情况:
89 |
90 | 
91 |
92 | 其中横坐标是每个Server的虚拟节点的数量,纵坐标是每个Server负载量的标准偏差,反映了Server之间负载的不均衡度,从图中看出,当虚拟节点的数量增加时,Server之间的不均衡度下降了。
93 |
94 | 虚拟节点除了用来降低Server的不均衡度之外,还可以用来表示每个Server的容量情况,例如,对于负载能力为1的Server,可以给它分配1个虚拟节点,而对于负载能力为10的Server,可以给它分配10个虚拟节点,从而为异构的Server提供相应的支持。
95 |
96 | 本文的代码在[consistent_hash.cpp](https://github.com/Charles0429/toys/blob/master/consistent-hash/consistent_hash.cpp),使用方法在[ReadMe](https://github.com/Charles0429/toys/tree/master/consistent-hash)。
97 |
98 | PS:
99 | 本博客更新会在第一时间推送到微信公众号,欢迎大家关注。
100 |
101 | 
102 |
103 | # 参考文献
104 |
105 | - [consistent hash](https://www.akamai.com/es/es/multimedia/documents/technical-publication/consistent-hashing-and-random-trees-distributed-caching-protocols-for-relieving-hot-spots-on-the-world-wide-web-technical-publication.pdf)
106 | - [amazon dynamo](http://cs.ucsb.edu/~ravenben/classes/276/papers/dynamo-sosp07.pdf)
107 | - [consistent hash by Tom](http://www.tom-e-white.com/2007/11/consistent-hashing.html)
108 |
--------------------------------------------------------------------------------
/doc/gfs.md:
--------------------------------------------------------------------------------
1 | # 1. Introduction
2 |
3 | 本文是读GFS论文的总结,收录在我的github中[papers项目](https://github.com/Charles0429/papers),papers项目旨在学习和总结分布式系统相关的论文。
4 |
5 | 全文主要分为以下几方面:
6 |
7 | - Design Motivation
8 | - Architecture
9 | - System Interactions
10 | - Master Operation
11 | - Fault Tolerance and Diagnose
12 | - Discussion
13 |
14 | # 2. Design Motivation
15 |
16 | google对现有系统的运行状态以及应用系统进行总结,抽象出对文件系统的需求,主要分为以下几个方面。
17 |
18 | - 普通商用的机器硬件发生故障是常态
19 | - 存储的问题普遍比较大,几个G的文件很常见
20 | - 大部分的文件操作都是在追加数据,覆盖原来写入的数据的情况比较少见,随机写几乎不存在
21 | - 读操作主要包括两种,large streaming read和small random read
22 | - 为了应用使用方便,多客户端并行地追加同一个文件需要非常高效
23 | - 带宽的重要性大于时延,目标应用是高速读大块数据的应用,对响应时间没有过多的需求
24 |
25 | # 3. Architecture
26 |
27 | 本部分讨论gfs的总体架构,以及在此架构上需要考虑的一些问题。
28 |
29 | ##3.1 Overview
30 |
31 | GFS的整体架构如下图:
32 |
33 | 
34 |
35 | (图片来源:gfs论文)
36 |
37 | GFS中有四类角色,分别是
38 |
39 | - GFS chunkserver
40 | - GFS master
41 | - GFS client
42 | - Application
43 |
44 | ###3.1.1 GFS chunkserver
45 |
46 | 在GFS chunkserver中,文件都是分成固定大小的chunk来存储的,每个chunk通过全局唯一的64位的chunk handle来标识,chunk handle在chunk创建的时候由GFS master分配。GFS chunkserver把文件存储在本地磁盘中,读或写的时候需要指定文件名和字节范围,然后定位到对应的chunk。为了保证数据的可靠性,一个chunk一般会在多台GFS chunkserver上存储,默认为3份,但用户也可以根据自己的需要修改这个值。
47 |
48 | ### 3.1.2 GFS master
49 |
50 | GFS master管理所有的元数据信息,包括namespaces,访问控制信息,文件到chunk的映射信息,以及chunk的地址信息(即chunk存放在哪台GFS chunkserver上)。
51 |
52 | ### 3.1.3 GFS client
53 |
54 | GFS client是GFS应用端使用的API接口,client和GFS master交互来获取元数据信息,但是所有和数据相关的信息都是直接和GFS chunkserver来交互的。
55 |
56 | ### 3.1.4 Application
57 |
58 | Application为使用gfs的应用,应用通过GFS client与gfs后端(GFS master和GFS chunkserver)打交道。
59 |
60 | ## 3.2 Single Master
61 |
62 | GFS架构中只有单个GFS master,这种架构的好处是设计和实现简单,例如,实现负载均衡时可以利用master上存储的全局的信息来做决策。但是,在这种架构下,要避免的一个问题是,应用读和写请求时,要弱化GFS master的参与度,防止它成为整个系统架构中的瓶颈。
63 |
64 | 从一个请求的流程来讨论上面的问题。首先,应用把文件名和偏移量信息传递给GFS client,GFS client转换成(文件名,chunk index)信息传递给GFS master,GFS master把(chunk handle, chunk位置信息)返回给客户端,客户端会把这个信息缓存起来,这样,下次再读这个chunk的时候,就不需要去GFS master拉取chunk位置信息了。
65 |
66 | 另一方面,GFS支持在一个请求中同时读取多个chunk的位置信息,这样更进一步的减少了GFS client和GFS master的交互次数,避免GFS master成为整个系统的瓶颈。
67 |
68 | ## 3.3 Chunk Size
69 |
70 | 对于GFS来说,chunk size的默认大小是64MB,比一般文件系统的要大。
71 |
72 | **优点**
73 |
74 | - 可以减少GFS client和GFS master的交互次数,chunk size比较大的时候,多次读可能是一块chunk的数据,这样,可以减少GFS client向GFS master请求chunk位置信息的请求次数。
75 | - 对于同一个chunk,GFS client可以和GFS chunkserver之间保持持久连接,提升读的性能。
76 | - chunk size越大,chunk的metadata的总大小就越小,使得chunk相关的metadata可以存储在GFS master的内存中。
77 |
78 | **缺点**
79 |
80 | - chunk size越大时,可能对部分文件来讲只有1个chunk,那么这个时候对该文件的读写就会落到一个GFS chunkserver上,成为热点。
81 |
82 | 对于热点问题,google给出的解决方案是应用层避免高频地同时读写同一个chunk。还提出了一个可能的解决方案是,GFS client找其他的GFS client来读数据。
83 |
84 | 64MB应该是google得出的一个比较好的权衡优缺点的经验值。
85 |
86 | ##3.4 Metadata
87 |
88 | GFS master存储三种metadata,包括文件和chunk namespace,文件到chunk的映射以及chunk的位置信息。这些metadata都是存储在GFS master的内存中的。对于前两种metadata,还会通过记操作日志的方式持久化存储,操作日志会同步到包括GFS master在内的多台机器上。GFS master不持久化存储chunk的位置信息,每次GFS master重启或者有新的GFS chunkserver加入时,GFS master会要求对应GFS chunkserver把chunk的位置信息汇报给它。
89 |
90 | ### 3.4.1 In-Memory Data Structures
91 |
92 | 使用内存存储metadata的好处是读取metadata速度快,方便GFS master做一些全局扫描metadata相关信息的操作,例如负载均衡等。
93 |
94 | 但是,以内存存储的的话,需要考虑的是GFS master的内存空间大小是不是整个系统能存储的chunk数量的瓶颈所在。在GFS实际使用过程中,这一般不会成为限制所在,因为GFS中一个64MBchunk的metadata大小不超过64B,并且,对于大部分chunk来讲都是使用的全部的空间的,只有文件的最后一个chunk会存储在部分空间没有使用,因此,GFS master的内存空间在实际上很少会成为限制系统容量的因素。即使真的是现有的存储文件的chunk数量超过了GFS master内存空间大小的限制,也可以通过加内存的方式,来获取内存存储设计带来的性能、可靠性等多种好处。
95 |
96 | ### 3.4.2 Chunk Locations
97 |
98 | GFS master不持久化存储chunk位置信息的原因是,GFS chunkserver很容易出现宕机,重启等行为,这样GFS master在每次发生这些事件的时候,都要修改持久化存储里面的位置信息的数据。
99 |
100 | ### 3.4.3 Operation Log
101 |
102 | **operation log的作用**
103 |
104 | - 持久化存储metadata
105 | - 它的存储顺序定义了并行的操作的最终的操作顺序
106 |
107 | **怎么存**
108 |
109 | operation log会存储在GFS master和多台远程机器上,只有当operation log在GFS master和多台远程机器都写入成功后,GFS master才会向GFS client返回成功。为了减少operation log在多台机器落盘对吞吐量的影响,可以将一批的operation log形成一个请求,然后写入到GFS master和其他远程机器上。
110 |
111 | **check point**
112 |
113 | 当operation log达到一定大小时,GFS master会做checkpoint,相当于把内存的B-Tree格式的信息dump到磁盘中。当master需要重启时,可以读最近一次的checkpoint,然后replay它之后的operation log,加快恢复的时间。
114 |
115 | 做checkpoint的时候,GFS master会先切换到新的operation log,然后开新线程做checkpoint,所以,对新来的请求是基本是不会有影响的。
116 |
117 | # 4. System Interactions
118 |
119 | 本部分讨论GFS的系统交互流程。
120 |
121 | ## 4.1 Leases and Mutation Order
122 |
123 | GFS master对后续的数据流程是不做控制的,所以,需要一个机制来保证,所有副本是按照同样的操作顺序写入对应的数据的。GFS采用lease方式来解决这个问题,GFS对一个chunk会选择一个GFS chunkserver,发放lease,称作primary,由primary chunkserver来控制写入的顺序。
124 |
125 | Lease的过期时间默认是60s,可以通过心跳信息来续时间,如果一个primary chunkserver是正常状态的话,这个时间一般是无限续下去的。当primary chunkserver和GFS master心跳断了后,GFS master也可以方便的把其他chunk副本所在的chunkserver设置成primary。
126 |
127 | ### 4.1.1 Write Control and Data Flow
128 |
129 | 
130 |
131 | (图片来源:gfs论文)
132 |
133 | 1. GFS client向GFS master请求拥有具有当前chunk的lease的chunkserver信息,以及chunk的其他副本所在的chunkserver的信息,如果当前chunk没有lease,GFS master会分配一个。
134 | 2. GFS master把primary chunkserver以及其他副本的chunkserver信息返回给client。client会缓存这些信息,只有当primary chunkserver连不上或者lease发生改变后,才需要再向GFS master获取对应的信息。
135 | 3. client把数据推送给所有包含此chunk的chunkserver,chunkserver收到后会先把数据放到内部的LRU buffer中,当数据被使用或者过期了,才删除掉。注意,这里没有将具体怎么来发送数据,会在下面的Data Flow讲。
136 | 4. 当所有包含chunk副本的chunkserver都收到了数据,client会给primary发送一个写请求,包含之前写的数据的信息,primary会分配对应的序号给此次的写请求,这样可以保证从多个客户端的并发写请求会得到唯一的操作顺序,保证多个副本的写入数据的顺序是一致的。
137 | 5. primary转发写请求给所有其他的副本所在的chunkserver(Secondary replica),操作顺序由primary指定。
138 | 6. Secondary replica写成功后会返回给primary replica。
139 | 7. Primary replica返回给client。任何副本发生任何错误都会返回给client。
140 |
141 | 这里,写数据如果发生错误可能会产生不一致的情况,会在consistency model中讨论。
142 |
143 | ## 4.2 Data Flow
144 |
145 | 4.1中第三步的Data Flow采用的是pipe line方式,目标是为了充分利用每台机器的网络带宽。假设一台机器总共有三个副本S1-S3。整个的Data Flow为:
146 |
147 | 1. client选择离它最近的chunkserver S1,开始推送数据
148 | 2. 当chunkserver S1收到数据后,它会立马转发到离它最近的chunkserver S2
149 | 3. chunkserver S2收到数据后,会立马转发给离它最近的chunkserver S3
150 |
151 | 不断重复上述流程,直到所有的chunkserver都收到client的所有数据。
152 |
153 | 以上述方式来传送B字节数据到R个副本,并假设网络吞吐量为T,机器之间的时延为L,那么,整个数据的传输时间为B/T+RL。
154 |
155 | ## 4.3 Atomic Record Appends
156 |
157 | Append操作流程和写差不多,主要区别在以下
158 |
159 | - client把数据推送到所有副本的最后一个chunk,然后发送写请求到primary
160 | - primary首先检查最后一个chunk的剩余空间是否可以满足当前写请求,如果可以,那么执行写流程,否则,它会把当前的chunk的剩余空间pad起来,然后告诉其他的副本也这么干,最后告诉client这个chunk满了,写入下个chunk。
161 |
162 | 这里需要讨论的是,如果append操作在部分副本失败的情况下,会发生什么?
163 |
164 | 例如,写操作要追加到S1-S3,但是,仅仅是S1,S2成功了,S3失败了,GFS client会重试操作,假如第二次成功了,那么S1,S2写了两次,S3写了一次,目前的理解是GFS会先把失败的记录进行padding对齐到primary的记录,然后再继续append。
165 |
166 | ## 4.4 Snapshot
167 |
168 | Snapshot的整个流程如下:
169 |
170 | 1. client向GFS master发送Snapshot请求
171 | 2. GFS master收到请求后,会回收所有这次Snapshot涉及到的chunk的lease
172 | 3. 当所有回收的lease到期后,GFS master写入一条日志,记录这个信息。然后,GFS会在内存中复制一份snapshot涉及到的metadata
173 |
174 | 当snapshot操作完成后,client写snapshot中涉及到的chunk C的流程如下:
175 |
176 | 1. client向GFS master请求primary chunkserver和其他chunkserver
177 | 2. GFS master发现chunk C的引用计数超过1,即snapshot和本身。它会向所有有chunk C副本的chunkserver发送创建一个chunk C的拷贝请求,记作是chunk C',这样,把最新数据写入到chunk C'即可。本质上是copy on write。
178 |
179 | ## 4.5 Consistency Model
180 |
181 | 
182 |
183 | (图片来源:gfs论文)
184 |
185 | GFS中consistent、defined的定义如下:
186 |
187 | - consistent:所有的客户端都能看到一样的数据,不管它们从哪个副本读取
188 | - defined:当一个文件区域发生操作后,client可以看到刚刚操作的所有数据,那么说这次操作是defined。
189 |
190 | 下面分析表格中出现的几种情况。
191 |
192 | 1. Write(Serial Success),单个写操作,并且返回成功,那么所有副本都写入了这次操作的数据,因此所有客户端都能看到这次写入的数据,所以,是defined。
193 | 2. Write(Concurrent Successes),多个写操作,并且返回成功,由于多个客户端写请求发送给priamary后,由primary来决定写的操作顺序,但是,有可能多个写操作可能是有区域重叠的,这样,最终写完成的数据可能是多个写操作数据叠加在一起,所以这种情况是consistent和undefined。
194 | 3. Write(Failure),写操作失败,则可能有的副本写入了数据,有的没有,所以是inconsistent。
195 | 4. Record Append(Serial Success and Concurrent Success),由于Record Append可能包含重复数据,因此,是inconsistent,由于整个写入的数据都能看到,所以是defined。
196 | 5. Record Append(Failure),可能部分副本append成功,部分副本append失败,所以,结果是inconsistent。
197 |
198 | GFS用version来标记一个chunkserver挂掉的期间,是否有client进行了write或者append操作。每进行一次write或者append,version会增加。
199 |
200 | 需要考虑的点是client会缓存chunk的位置信息,有可能其中某些chunkserver已经挂掉又起来了,这个时候chunkserver的数据可能是老的数据,读到的数据是会不一致的。读流程中,好像没有看到要带version信息来读的。这个论文中没看到避免的措施,目前还没有结果。
201 |
202 | ###4.5.1 Implications for Applications
203 |
204 | 应用层需要采用的机制:用append而不是write,做checkpoint,writing self-validating和self-identifying records。具体地,如下:
205 |
206 | 1. 应用的使用流程是append一个文件,到最终写完后,重命名文件
207 | 2. 对文件做checkpoint,这样应用只需要关注上次checkpoint时的文件区域到最新文件区域的数据是否是consistent的,如果这期间发生不一致,可以重新做这些操作。
208 | 3. 对于并行做append的操作,可能会出现重复的数据,GFS client提供去重的功能。
209 |
210 | # 5. Master Operation
211 |
212 | GFS master的功能包括,namespace Management, Replica Placement,Chunk Creation,Re-replication and Rebalancing以及Garbage Collection。
213 |
214 | ## 5.1 Namespace Management and Locking
215 |
216 | 每个master操作都需要获得一系列的锁。如果一个操作涉及到/d1/d2/.../dn/leaf,那么需要获得/d1,/d1/d2,/d1/d2/.../dn的读锁,然后,根据操作类型,获得/d1/d2/.../dn/leaf的读锁或者写锁,其中leaf可能是文件或者路径。
217 |
218 | 一个例子,当/home/user被快照到/save/user的时候,/home/user/foo的创建是被禁止的。
219 |
220 | 对于快照,需要获得/home和/save的读锁,/home/user和/save/user的写锁。对于创建操作,会获得/home,/home/user的读锁,然后/home/user/foo的写锁。其中,/home/user的锁产生冲突,/home/user/foo创建会被禁止。
221 |
222 | 这种加锁机制的好处是对于同一个目录下,可以并行的操作文件,例如,同一个目录下并行的创建文件。
223 |
224 | ## 5.2 Replica Placement
225 |
226 | GFS的Replica Placement的两个目标:最大化数据可靠性和可用性,最大化网络带宽的使用率。因此,把每个chunk的副本分散在不同的机架上,这样一方面,可以抵御机架级的故障,另一方面,可以把读写数据的带宽分配在机架级,重复利用多个机架的带宽。
227 |
228 | ## 5.3 Creation, Re-replication, Rebalancing
229 |
230 | ###5.3.1 Chunk Creation
231 |
232 | GFS在创建chunk的时候,选择chunkserver时考虑的因素包括:
233 |
234 | 1. 磁盘空间使用率低于平均值的chunkserver
235 | 2. 限制每台chunkserver的最近的创建chunk的次数,因为创建chunk往往意味着后续需要写大量数据,所以,应该把写流量尽量均摊到每台chunkserver上
236 | 3. chunk的副本放在处于不同机架的chunkserver上
237 |
238 | ## 5.3.2 Chunk Re-replication
239 |
240 | 当一个chunk的副本数量少于预设定的数量时,需要做复制的操作,例如,chunkserver宕机,副本数据出错,磁盘损坏,或者设定的副本数量增加。
241 |
242 | chunk的复制的优先级是按照下面的因素来确定的:
243 |
244 | 1. 丢失两个副本的chunk比丢失一个副本的chunk的复制认为优先级高
245 | 2. 文件正在使用比文件已被删除的chunk的优先级高
246 | 3. 阻塞了client进程的chunk的优先级高(这个靠什么方法得到?)
247 |
248 | chunk复制的时候,选择新chunkserver要考虑的点:
249 |
250 | 1. 磁盘使用率
251 | 2. 单个chunkserver的复制个数限制
252 | 3. 多个副本需要在多个机架
253 | 4. 集群的复制个数限制
254 | 5. 限制每个chunkserver的复制网络带宽,通过限制读流量的速率来限制
255 |
256 | ### 5.3.3 Rebalancing
257 |
258 | 周期性地检查副本分布情况,然后调整到更好的磁盘使用情况和负载均衡。GFS master对于新加入的chunkserver,逐渐地迁移副本到上面,防止新chunkserver带宽打满。
259 |
260 | ## 5.4 Garbage Collection
261 |
262 | 在GFS删除一个文件后,并不会马上就对文件物理删除,而是在后面的定期清理的过程中才真正的删除。
263 |
264 | 具体地,对于一个删除操作,GFS仅仅是写一条日志记录,然后把文件命名成一个对外部不可见的名称,这个名称会包含删除的时间戳。GFS master会定期的扫描,当这些文件存在超过3天后,这些文件会从namespace中删掉,并且内存的中metadata会被删除。
265 |
266 | 在对chunk namespace的定期扫描时,会扫描到这些文件已经被删除的chunk,然后会把metadata从磁盘中删除。
267 |
268 | 在与chunkserver的heartbeat的交互过程中,GFS master会把不在metadata中的chunk告诉chunkserver,然后chunkserver就可以删除这些chunk了。
269 |
270 | 采用这种方式删除的好处:
271 |
272 | 1. 利用心跳方式交互,在一次删除失败后,还可以通过下次心跳继续重试操作
273 | 2. 删除操作和其他的全局扫描metadata的操作可以放到一起做
274 |
275 | 坏处:
276 |
277 | 1. 有可能有的应用需要频繁的创建和删除文件,这种延期删除方式会导致磁盘使用率偏高,GFS提供的解决方案是,对一个文件调用删除操作两次,GFS会马上做物理删除操作,释放空间。
278 |
279 | ##5.5 Stale Replication Detection
280 |
281 | 当一台chunkserver挂掉的时候,有新的写入操作到chunk副本,会导致chunkserve的数据不是最新的。
282 |
283 | 当master分配lease到一个chunk时,它会更新chunk version number,然后其他的副本都会更新该值。这个操作是在返回给客户端之前完成的,如果有一个chunkserver当前是宕机的,那么它的version number就不会增加。当chunkserver重启后,会汇报它的chunk以及version number,对于version number落后的chunk,master就认为这个chunk的数据是落后的。
284 |
285 | GFS master会把落后的chunk当垃圾来清理掉,并且不会把落后的chunkserver的位置信息传给client。
286 |
287 | 备注:
288 |
289 | 1. GFS master把落后的chunk当作垃圾清理,那么,是否是走re-replication的逻辑来生成新的副本呢?没有,是走立即复制的逻辑。
290 |
291 | #6. Fault Tolerance and Diagnose
292 |
293 | ## 6.1 High Availability
294 |
295 | 为了实现高可用性,GFS在通过两方面来解决,一是fast recovery,二是replication
296 |
297 | ###6.1.1 Fast Recovery
298 |
299 | master和chunkserver都被设计成都能在秒级别重启
300 |
301 | ### 6.1.2 Chunk Replications
302 |
303 | 每个chunk在多个机架上有副本,副本数量由用户来指定。当chunkserver不可用时,GFS master会自动的复制副本,保证副本数量和用户指定的一致。
304 |
305 | ###6.1.3 Master Replication
306 |
307 | master的operation log和checkpoint都会复制到多台机器上,要保证这些机器的写都成功了,才认为是成功。只有一台master在来做garbage collection等后台操作。当master挂掉后,它能在很多时间内重启;当master所在的机器挂掉后,监控会在其他具有operation log的机器上重启启动master。
308 |
309 | 新启动的master只提供读服务,因为可能在挂掉的一瞬间,有些日志记录到primary master上,而没有记录到secondary master上(这里GFS没有具体说同步的流程)。
310 |
311 | ## 6.2 Data Integrity
312 |
313 | 每个chunkserver都会通过checksum来验证数据是否损坏的。
314 |
315 | 每个chunk被分成多个64KB的block,每个block有32位的checksum,checksum在内存中和磁盘的log中都有记录。
316 |
317 | 对于读请求,chunkserver会检查读操作所涉及block的所有checksum值是否正确,如果有一个block的checksum不对,那么会报错给client和master。client这时会从其他副本读数据,而master会clone一个新副本,当新副本clone好后,master会删除掉这个checksum出错的副本。
318 |
319 | ## 6.3 Diagnose Tools
320 |
321 | 主要是通过log,包括重要事件的log(chunkserver上下线),RPC请求,RPC响应等。
322 |
323 | # 7. Discussion
324 |
325 | 本部分主要讨论大规模分布式系统一书上,列出的关于gfs的一些问题,具体如下。
326 |
327 | ## 7.1 为什么存储三个副本?而不是两个或者四个?
328 |
329 | - 如果存储的是两个副本,挂掉一个副本后,系统的可用性会比较低,例如,如果另一个没有挂掉的副本出现网络问题等,整个系统就不可用了
330 | - 如果存储的是四个副本,成本比较高
331 |
332 | ## 7.2 chunk的大小为何选择64MB?这个选择主要基于哪些考虑?
333 |
334 | **优点**
335 |
336 | - 可以减少GFS client和GFS master的交互次数,chunk size比较大的时候,多次读可能是一块chunk的数据,这样,可以减少GFS client向GFS master请求chunk位置信息的请求次数。
337 | - 对于同一个chunk,GFS client可以和GFS chunkserver之间保持持久连接,提升读的性能。
338 | - chunk size越大,chunk的metadata的总大小就越小,使得chunk相关的metadata可以存储在GFS master的内存中。
339 |
340 | **缺点**
341 |
342 | - chunk size越大时,可能对部分文件来讲只有1个chunk,那么这个时候对该文件的读写就会落到一个GFS chunkserver上,成为热点。
343 |
344 | 64MB应该是google得出的一个比较好的权衡优缺点的经验值。
345 |
346 | ## 7.3 gfs主要支持追加,改写操作比较少,为什么这么设计?如何设计一个仅支持追加操作的文件系统来构建分布式表格系统bigtable?
347 |
348 | - 因为追加多,改写少是google根据现有应用需求而确定的
349 | - bigtable的问题等读到bigtable论文再讨论
350 |
351 | ## 7.4 为什么要将数据流和控制流分开?如果不分开,如何实现追加流程?
352 |
353 | 主要是为了更有效地利用网络带宽。把数据流分开,可以更好地优化数据流的网络带宽使用。
354 |
355 | 如果不分开,需要讨论下。
356 |
357 | ## 7.5 gfs有时会出现重复记录或者padding记录,为什么?
358 |
359 | **padding出现场景:**
360 |
361 | - last chunk的剩余空间不满足当前写入量大小,需要把last chunk做padding,然后告诉客户端写入下一个chunk
362 | - append操作失败的时候,需要把之前写入失败的副本padding对齐到master
363 |
364 | **重复记录出现场景:**
365 |
366 | - append操作部分副本成功,部分失败,然后告诉客户端重试,客户端会在成功的副本上再次append,这样就会有重复记录出现
367 |
368 | ## 7.6 lease是什么?在gfs中起到了什么作用?它与心跳有何区别?
369 |
370 | lease是gfs master把控制写入顺序的权限下放给chunkserver的机制,以减少gfs master在读写流程中的参与度,防止其成为系统瓶颈。心跳是gfs master检测chunkserver是否可用的标志。
371 |
372 | ## 7.7 gfs追加过程中如果出现备副本故障,如何处理?如果出现主副本故障,应该如何处理?
373 |
374 | - 对于备副本故障,写入的时候会失败,然后primary会返回错误给client。按照一般的系统设计,client会重试一定次数,发现还是失败,这时候client会把情况告诉给gfs master,gfs master可以检测chunkserver的情况,然后把最新的chunkserver信息同步给client,client端再继续重试。
375 |
376 |
377 | - 对于主副本故障,写入的时候会失败,client端应该是超时了。client端会继续重试一定次数,发现还是一直超时,那么把情况告诉给gfs master,gfs master发现primary挂掉,会重新grant lease到其他chunkserver,并把情况返回给client。
378 |
379 | ## 7.8 gfs master需要存储哪些信息?master的数据结构如何设计?
380 |
381 | namespace、文件到chunk的映射以及chunk的位置信息
382 |
383 | namespace采用的是B-Tree,对于名称采用前缀压缩的方法,节省空间;(文件名,chunk index)到chunk的映射,可以通过hashmap;chunk到chunk的位置信息,可以用multi_hashmap,因为是一对多的映射。
384 |
385 | ## 7.9 假设服务一千万个文件,每个文件1GB,master中存储元数据大概占多少内存?
386 |
387 | 1GB/64MB = 1024 / 64 = 16。总共需要16 * 10000000 * 64 B = 10GB
388 |
389 | ## 7.10 master如何实现高可用性?
390 |
391 | - metadata中namespace,以及文件到chunk信息持久化,并存储到多台机器
392 | - 对metadata的做checkpoint,保证重启后replay消耗时间比较短,checkpoint可以直接映射到内存使用,不用解析
393 | - 在primary master发生故障的时候,并且无法重启时,会有外部监控将secondary master,并提供读服务。secondary master也会监控chunkserver的状态,然后把primary master的日志replay到内存中
394 |
395 | ## 7.11 负载的影响因素有哪些?如何计算一台机器的负载值?
396 |
397 | 主要是考虑CPU、内存、网络和I/O,但如何综合这些参数并计算还是得看具体的场景,每部分的权重随场景的不同而不同。
398 |
399 | ## 7.12 master新建chunk时如何选择chunkserver?如果新机器上线,负载值特别低,如何避免其他chunkserver同时往这台机器上迁移chunk?
400 |
401 | **如何选择chunkserver**
402 |
403 | - 磁盘空间使用率低于平均值的chunkserver
404 | - 限制每台chunkserver最近创建chunk的次数,因为创建chunk往往意味着后续需要写入大量数据,所以,应该把写流量均摊到每台chunkserver
405 | - chunk的副本放置于不同机架的chunkserver上
406 |
407 | **如何避免同时迁移**
408 |
409 | 通过限制单个chunkserver的clone操作的个数,以及clone使用的带宽来限制,即从源chunkserver度数据的频率做控制。
410 |
411 | ## 7.13 如果chunkserver下线后过一会重新上线,gfs如何处理?
412 |
413 | 因为是过一会,所以假设chunk re-replication还没有执行,那么在这期间,可能这台chunkserver上有些chunk的数据已经处于落后状态了,client读数据的时候或者chunkserver定期扫描的时候会把这些状态告诉给master,master告诉上线后的chunkserver从其他机器复制该chunk,然后master会把这个chunk当作是垃圾清理掉。
414 |
415 | 对于没有落后的chunk副本,可以直接用于使用。
416 |
417 | ## 7.14 如何实现分布式文件系统的快照操作?
418 |
419 | Snapshot的整个流程如下:
420 |
421 | 1. client向GFS master发送Snapshot请求
422 | 2. GFS master收到请求后,会回收所有这次Snapshot涉及到的chunk的lease
423 | 3. 当所有回收的lease到期后,GFS master写入一条日志,记录这个信息。然后,GFS会在内存中复制一份snapshot涉及到的metadata
424 |
425 | 当snapshot操作完成后,client写snapshot中涉及到的chunk C的流程如下:
426 |
427 | 1. client向GFS master请求primary chunkserver和其他chunkserver
428 | 2. GFS master发现chunk C的引用计数超过1,即snapshot和本身。它会向所有有chunk C副本的chunkserver发送创建一个chunk C的拷贝请求,记作是chunk C',这样,把最新数据写入到chunk C'即可。本质上是copy on write。
429 |
430 | ## 7.15 chunkserver数据结构如何设计?
431 |
432 | chunkserver主要是存储64KB block的checksum信息,需要由chunk+offset,能够快速定位到checksum,可以用hashmap。
433 |
434 | ## 7.16 磁盘可能出现位翻转错误,chunkserver如何应对?
435 |
436 | 利用checksum机制,分读和写两种情况来讨论:
437 |
438 | 1. 对于读,要检查所读的所有block的checksum值
439 | 2. 对于写,分为append和write。对于append,不检查checksum,延迟到读的时候检查,因为append的时候,对于最后一个不完整的block计算checksum时候采用的是增量的计算,即使前面存在错误,也能在后来的读发现。对于overwrite,因为不能采用增量计算,要覆盖checksum,所以,必须要先检查只写入部分数据的checksum是否不一致,否则,数据错误会被隐藏。
440 |
441 | ## 7.17 chunkserver重启后可能有一些过期的chunk,master如何能够发现?
442 |
443 | chunkserver重启后,会汇报chunk及其version number,master根据version number来判断是否过期。如果过期了,那么会做以下操作:
444 |
445 | 1. 过期的chunk不参与数据读写流程
446 | 2. master会告诉chunkserver从其他的最新副本里拷贝一份数据
447 | 3. master将过期的chunk加入garbage collection中
448 |
449 | 问题:如果chunkserver拷贝数据的过程过程中,之前拷贝的数据备份又发生了变化,然后分为两种情况讨论:
450 |
451 | 1. 如果期间lease没变,那么chunkserver不知道自己拷贝的数据是老的,应该会存在不一致的问题?
452 | 2. 如果期间lease改变,那么chunkserver因为还不能提供读服务,那么version number应该不会递增,继续保持stable状态,然后再发起拷贝。
453 |
--------------------------------------------------------------------------------
/doc/mapreduce.md:
--------------------------------------------------------------------------------
1 | # 1. Introduction
2 |
3 | 本文是读MapReduce论文的总结。
4 |
5 | Google发现有一些应用的计算模型比较简单,但涉及到大量数据,需要成百上千的机器来处理。如何并行化计算、分布数据和处理故障需要复杂的处理呢?MapReduce的出现即为了解决这个问题。通过提供的编程库,用户能轻松地写出处理逻辑,而内部的并行化计算、数据分布等问题由MapReduce来处理,大大简化了用户的编程逻辑。
6 |
7 | MapReduce受到lisp等函数式编程语言的启发,发现大部分的计算任务包括两个处理流程:
8 |
9 | - map操作:对每条逻辑记录计算Key/Value对
10 | - reduce操作:对Key/Value按照Key进行聚合
11 |
12 | 接下来,按照如下结构分析MapReduce系统
13 |
14 | - Programming Model
15 | - Implementation
16 | - Refinements
17 |
18 | # 2 Programming Model
19 |
20 | MapReduce的计算以一组Key/Value对为输入,然后输出一组Key/Value对,用户通过编写Map和Reduce函数来控制处理逻辑。
21 |
22 | Map函数把输入转换成一组中间的Key/Value对,MapReduce library会把所有Key的中间结果传递给Reduce函数处理。
23 |
24 | Reduce函数接收Key和其对应的一组Value,它的作用就是聚合这些Value,产生最终的结果。Reduce的输入是以迭代器的方式输入,使得MapReduce可以处理数据量比内存大的情况。
25 |
26 | ## 2.1 Example
27 |
28 | 以经典的word count为例,其伪代码为
29 |
30 | ```
31 | map(String key, String value):
32 | // key: document name
33 | // value: document contents
34 | for each word w in value:
35 | EmitIntermediate(w, "1");
36 |
37 | reduce(String key, Iterator values):
38 | // key: a word
39 | // values: a list of counts
40 | int result = 0;
41 | for each v in values:
42 | result += ParseInt(v);
43 | Emit(AsString(result));
44 | ```
45 |
46 | Map函数吐出(word, count)的K/V对,Reduce把某个单词的所有的count加起来,最终每个单词吐出一个值。
47 |
48 | 除了Map和Reduce函数之外,用户还需要指定输入和输出文件名,以及一些可选的调节的参数。
49 |
50 | ## 2.2 Types
51 |
52 | Map和Reduce函数的操作可以抽象的表示为
53 |
54 | ```
55 | map (k1,v2) ======>list(k2,v2)
56 | reduce (k2, list(v2)) ======>list(v2)
57 | ```
58 |
59 | 如上所示,map函数生成一系列的K/V中间结果,然后reduce对每个key,聚合其value。
60 |
61 | ## 2.3 More Examples
62 |
63 | **Distributed Grep**
64 |
65 | - 对于map,如果输入的行匹配到相应的pattern,则吐出这行
66 | - 对于reduce,仅仅是把map吐出的行拷贝到输出中
67 |
68 | **Count of URL Access Frequency**
69 |
70 | - 对于map,处理web日志,生成(URL, 1)中间结果
71 | - 对于reduce,聚合相同URL的值,生成(URL, total count)结果
72 |
73 | **Reverse Web-Link Graph**
74 |
75 | - 对于map,吐出(target, source)中间结果,其中target是被source引用的URL
76 | - 对于reduce,聚合相同target的source,吐出(target, list(source))
77 |
78 | **Term-Vector per Host**
79 |
80 | Term Vector指的是一篇文档中的(word, frequency)K/V对。
81 |
82 | - 对于map,吐出(hostname, term vector)中间结果
83 | - 对于reduce,聚合相同hostname的term vector,吐出最终(hostname, term vector)
84 |
85 | **Inverted Index**
86 |
87 | - 对于map,吐出一系列的(word, document ID)
88 | - 对于reduce,对相同word,按照document ID排序进行聚合,吐出(word, list(document ID))
89 |
90 | **Distributed Sort**
91 |
92 | - 对于map,吐出(key, record)中间结果
93 | - 对于reduce,把map的中间结果写入到结果文件中,这里不需要显式地排序,因为MapReduce会自动地排序,方便在reduce的时候进行聚合。
94 |
95 | # 3. Implementation
96 |
97 | 根据不同的环境,MapReduce的实现可以多种多样,例如,基于共享内存的,基于NUMA多核环境的,以及基于多台机器组成的集群环境的。
98 |
99 | Google的环境如下
100 |
101 | - 双核X86系统,运行linux系统,2-4GB内存。
102 | - 100M或1000M带宽网卡
103 | - 集群由大量机器组成,故障是常态
104 | - 每台机器使用廉价的IDE磁盘,采用GFS作为底层存储
105 | - 使用一个调度系统来处理用户的任务
106 |
107 | ## 3.1 Execution Overview
108 |
109 | Map会自动地把输入数据划分成M份,这些数据划分可以并行地被不同机器处理。Reduce按照划分函数划分数据,例如hash(key) mod R,其中R是由用户指定的。下图描述了MapReduce的整个流程,如下
110 |
111 | 
112 |
113 | 1. MapReduce library会把输入文件划分成多个16到64MB大小的分片(大小可以通过参数调节),然后在一组机器上启动程序。
114 | 2. 其中比较特殊的程序是master,剩下的由master分配任务的程序叫worker。总共有M个map任务和R个reduce任务需要分配,master会选取空闲的worker,然后分配一个map任务或者reduce任务。
115 | 3. 处理map任务的worker会从输入分片读入数据,解析出输入数据的K/V对,然后传递给Map函数,生成的K/V中间结果会缓存在内存中。
116 | 4. map任务的中间结果会被周期性地写入到磁盘中,以partition函数来分成R个部分。R个部分的磁盘地址会推送到master,然后由它转发给响应的reduce worker。
117 | 5. 当reduce worker接收到master发送的地址信息时,它会通过RPC来向map worker读取对应的数据。当reduce worker读取到了所有的数据,它先按照key来排序,方便聚合操作。
118 | 6. reduce worker遍历排序好的中间结果,对于相同的key,把其所有数据传入到Reduce函数进行处理,生成最终的结果会被追加到结果文件中。
119 | 7. 当所有的map和reduce任务都完成时,master会唤醒用户程序,然后返回到用户程序空间执行用户代码。
120 |
121 | 成功执行后,输出结果在R个文件中,通常,用户不需要合并这R个文件,因为,可以把它们作为新的MapReduce处理逻辑的输入数据,或者其它分布式应用的输入数据。
122 |
123 | ## 3.2 Master Data Structure
124 |
125 | master维护了以下信息
126 |
127 | - 对每个map和reduce任务,记录了任务状态,包括idle,in-progress或completed,并且对于非idle状态的任务还记录了worker机器的信息
128 | - 记录了map任务生成R个部分的文件位置信息
129 |
130 | ## 3.3 Fault Tolerance
131 |
132 | 分为两块,worker fault tolerance和master fault tolerance
133 |
134 | **Worker Failure**
135 |
136 | master采用ping的方式检测故障,如果一台worker机器在一定时间内没有响应,则认为这台机器故障。
137 |
138 | - 对于map任务机器故障,完成了的map任务也需要完全重新执行,因为计算结果是存储在map任务所在机器的本地磁盘上的
139 |
140 | 当一个map任务开始由A来执行,而后挂掉后由B来执行,所有的为接收改任务数据的reduce任务的机器都会收到新的通知。
141 |
142 | - 对于完成了的reduce任务则不需要重新执行,因为结果已经输出到GFS中
143 |
144 | **Master Failure**
145 |
146 | 可以通过定期的checkpoint来保存状态,master挂掉后,可以回到最近checkpoint所在的状态。
147 |
148 | 但google没有采用这种方案,因为任务master挂掉概率极小,只需要让应用重试这次操作。
149 |
150 | **Semantics in the Presence of Failure**
151 |
152 | 当用户提供的Map和Reduce函数的执行结果是确定的,那么最终的执行结果就是确定的。
153 |
154 | 当用户提供的执行结果不是确定的,那么最终结果也是不确定的,但是每个reduce任务产生的结果都是不确定的某次串行执行的结果。
155 |
156 | ## 3.4 Locality
157 |
158 | 由于输入数据是存储在GFS上的,所以,MapReduce为了减少网络通信,采取了以下优化策略
159 |
160 | 1. 因为GFS是按照64MB的chunk来存储数据的,这样可以把worker按照这个信息调度,尽量是每个worker都起到相应的GFS副本上,这样输入基本上是走本地磁盘
161 | 2. 如果上面的条件无法满足,那么尽量找一台和GFS副本机器在同一个交换机的机器
162 |
163 | ## 3.5 Task Granularity
164 |
165 | MapReduce将map任务分成M份,reduce任务分成R份,理想状态M和R的值应该比worker机器大很多,这样有助于负载均衡以及故障恢复。因为当一台机器挂掉后,它的map任务可以分配给很多其他的机器执行。
166 |
167 | 实际应用中,因为master需要O(M+R)的空间来做调度决策,需要存储O(M*R)的任务产生的结果位置信息,对于每个任务产生的结果位置信息大约每个任务需要一个字节。
168 |
169 | 通常R的数量是由用户执行的,实际应用中对M的划分是要保证一个分片的数据量大小大约是16-64M,R的期望值是一个比较小的数。典型的M和R的值为 M = 200000,R = 5000,使用2000台worker机器。
170 |
171 | ## 3.6 Backup Tasks
172 |
173 | 通常,在执行过程中,会有少数几台机器的执行特别慢,可能是由于磁盘故障等原因引起的,这些机器会大大地增加任务的执行时间,MapReduce采用的方案是
174 |
175 | - 当一个MapReduce操作快执行完成的时候,master会生成正在进行的任务的备份任务。备份任务和源任务做的是同样的事情,只要其中一个任务执行完成,就认为该任务执行完成。
176 |
177 | 该机制在占有很少的计算资源的情况下,大大缩短了任务的执行时间。
178 |
179 | # 4. Refinements
180 |
181 | 本节描述了一些提升效率的策略。
182 |
183 | ## 4.1 Partitioning Function
184 |
185 | map任务的中间结果按照partitioning function分成了R个部分,通常,默认的函数`hash(key) mod R`可以提供相对均衡的划分。但有时应用需要按照自己的需求的来划分,比如,当Key是URL时,用户可能希望相同host的URL划分到一起,方便处理。这时候,用户可以自己提供partitioning function,例如`hash(Hostname(url))`。
186 |
187 | ## 4.2 Ordering Guarantees
188 |
189 | 对于reduce任务生成的结果,MapReduce保证其是按照Key排序的,方便reduce worker聚合结果,并且还有两个好处
190 |
191 | - 按照key随机读性能较好
192 | - 用户程序需要排序时会比较方便
193 |
194 | ## 4.3 Combiner Function
195 |
196 | 在有些情况下,map任务生成的中间结果中key的重复度很高,会造成对应的reduce任务通信量比较大。例如,word count程序中,可能和the相关的单词量特别大,组成了很多的(the, 1)K/V对,这些都会推送到某个reduce任务,会造成该reduce任务通信量和计算量高于其他的reduce任务。解决的方法是
197 |
198 | - 在map任务将数据发送到网络前,通过提供一个`combiner`函数,先把数据做聚合,以减少数据在网络上的传输量
199 |
200 |
201 | ## 4.4 Input and Output Types
202 |
203 | MapReduce提供多种读写格式的支持,例如,文件中的偏移和行内容组成K/V对。
204 |
205 | 用户也可以自定义读写格式的解析,实现对应的接口即可。
206 |
207 | ## 4.5 Side-effects
208 |
209 | MapReduce允许用户程序生成辅助的输出文件,其原子性依赖于应用的实现。
210 |
211 | ## 4.6 Skipping Bad Records
212 |
213 | 有时候,可能用户程序有bug,导致任务在解析某些记录的时候会崩溃。普通的做法是修复用户程序的bug,但有时候,bug是来自第三方的库,无法修改源码。
214 |
215 | MapReduce的做法是通过监控任务进程的segementation violation和bus error信号,一旦发生,把响应的记录发送到master,如果master发现某条记录失败次数大于1,它就会在下次执行的时候跳过该条记录。
216 |
217 | ## 4.7 Local Execution
218 |
219 | 因为Map和Reduce任务是在分布式环境下执行的,要调试它们是非常困难的。MapReduce提供在本机串行化执行MapReduce的接口,方便用户调试。
220 |
221 | ## 4.8 Status Information
222 |
223 | master把内部的状态通过网页的方式展示出来,例如,计算的进度,包括,多少任务完成了,多少正在执行,输入的字节数,输出的中间结果,最终输出的字节数等;网页还包括每个任务的错误输出和标准输出,用户可以通过这些来判断计算需要的时间等;除此之外,还有worker失败的信息,方便排查问题。
224 |
225 | ## 4.9 Counters
226 |
227 | MapReduce libaray提供一个counter接口来记录各种事件发生的次数。
228 |
229 | 例如,word count用户想知道总共处理了多少大写单词,可以按照如下方式统计
230 |
231 | ```
232 | Counter* uppercase;
233 | uppercase = GetCounter("uppercase");
234 |
235 | map(String name, String contents):
236 | for each word w in contents:
237 | if (IsCapitalized(w)):
238 | uppercase->Increment();
239 | EmitIntermediate(w, "1");
240 | ```
241 |
242 | master通过ping-pong消息来拉取worker的count信息,当MapReduce操作完成时,count值会返回给用户程序,需要注意的是,重复执行的任务的count只会统计一次。
243 |
244 | 有些counter是MapReduce libaray内部自动维护的,例如,输入的K/V对数量,输出的K/V对数量等。
245 |
246 | Counter机制在有些情况很有用,比如用户希望输入和输出的K/V数量是完全相同的,就可以通过Counter机制来检查。
--------------------------------------------------------------------------------
/doc/newsql.md:
--------------------------------------------------------------------------------
1 | 近几年来,数据库领域出现了一种新的关系数据库类型,称为NewSQL,例如,Google的Spanner,Amazon的Aurora等等,这些数据库相对于传统数据库来讲,区别在哪里?[What's Really New with NewSQL?](http://db.cs.cmu.edu/papers/2016/pavlo-newsql-sigmodrec2016.pdf)给了很好的总结,本篇文章主要是总结该论文的观点,最后会有一个简单的讨论部分,全文的组织结构如下:
2 |
3 | - 为什么需要NewSQL?
4 | - NewSQL的分类
5 | - NewSQL的技术挑战有哪些?
6 | - 讨论
7 |
8 | 本文收录在我的github中[papers项目](https://github.com/Charles0429/papers),papers项目旨在学习和总结分布式系统相关的论文。
9 |
10 | # 为什么需要NewSQL?
11 |
12 | 数据库的发展通常是随着业务需求的变化,在2000年左右,随着互联网的兴起,有许多同时在线的用户,这对数据库领域带来了非常大的挑战,数据库通常会成为瓶颈,所以,此时业务针对数据库的需求,主要体现在可扩展上面。
13 |
14 | 这时期数据库的扩展性,往往采用如下两种方案:
15 |
16 | 1. 垂直扩展:使用更好的硬件,来做数据库的服务器
17 | 2. 水平扩展:采用中间件,做sharding的方式,即分库分表的方式
18 |
19 | 垂直扩展中使用更好的硬件意味者成本高,并且更换硬件后,需要把数据从老的机器迁移到新的机器,中间可能需要停服务,因此往往采用水平扩展,例如,Google's MySQL-based cluster。
20 |
21 | 采用中间件方式也有缺点,中间件一般要求轻量级,简单数据库操作可以搞定,但是,如果需要做分布式事务或者联表操作,会非常复杂,通常这些逻辑会放到应用层来做。
22 |
23 | 后续,NOSQL兴起,主要有几个原因:
24 |
25 | 1. 传统关系数据库更倾向于一致性,而在性能和可用性比较差
26 | 2. 全功能的关系型数据库太重
27 | 3. 关系模型对于简单的查询太重,不必要
28 |
29 | NOSQL以Google’s BigTable 和 Amazon’s Dynamo为代表,开源版对应为HBase和Cassandra。
30 |
31 | NOSQL往往是不保证强一致性的,而对于一些应用来讲(例如金融服务),是需要强一致性和事务的,因此,如果它们基于NOSQL系统来开发的话,应用层需要些大量的逻辑来处理一致性和事务相关的问题。此时,业务需求是拥有可扩展性的基础上,能够支持强一致性。
32 |
33 | 因此,这里有几条路:
34 |
35 | 1. 性能更好的单个服务器来做数据库服务器
36 | 2. 中间件层支持分布式事务
37 |
38 | 使用更好的单个服务器的话,不满足业务需求的可扩展性。
39 |
40 | 使用中间件的话,会有如下问题,例如:
41 |
42 | 1. 中间件层往往是比较轻量级的,要实现一致性,必须在中间件层实现分布式事务,这点是非常困难的
43 | 2. 中间件层本身的高可用很难保证
44 |
45 | 上面两条路都不能很好的满足应用的需求,因此,NewSQL出现了。
46 |
47 | 首先来看NEWSQL的定义:针对OLTP的读写,提供与NOSQL相同的可扩展性和性能,同时能支持满足ACID特性的事务。即保持NOSQL的高可扩展和高性能,并且保持关系模型。
48 |
49 | NEWSQL的优点:
50 |
51 | 1. 轻松的获得可扩展性
52 | 2. 能够使用关系模型和事务,应用逻辑会简化很多
53 |
54 | 注意,此篇论文中的NEWSQL偏向于OLTP型数据库,和一些OLAP类型的数据库不同,OLAP数据库更偏向于复杂的只读查询,查询时间往往很长。
55 |
56 | 而NEWSQL数据库的特性如下,针对其读写事务:
57 |
58 | 1. 执行时间短
59 | 2. 一般只查询一小部分数据,通过使用索引来达到高效查询的目的
60 | 3. 一般执行相同的命令,使用不同的输入参数
61 |
62 | # NewSQL的分类
63 |
64 | 分三大类:
65 |
66 | 1. 从头开始,使用新架构的系统
67 | 2. 中间件
68 | 3. DAAS,数据库即服务
69 |
70 | ## New Architectures
71 |
72 | 采用新架构的NewSQL有如下特点:
73 |
74 | 1. 无共享存储
75 | 2. 多节点的并发控制
76 | 3. 基于多副本做高可用和容灾
77 | 4. 流量控制
78 | 5. 分布式查询处理
79 |
80 | 优势:
81 |
82 | 1. 所有的部分都可以为分布式环境做优化,例如查询优化,通信协议优化。例如,所有的NEWSQL DBMS可以直接在节点间发送查询,而不是通过中心节点,例如中间件系统
83 | 2. 本身负责数据分区,因此,可以把查询发送给有数据的分区,而不是把数据发送给查询。
84 | 3. 拥有自身的存储,可以指定更复杂的多副本的方式
85 |
86 | 缺点:
87 |
88 | 1. 懂该数据库的人少,缺少专业的运维
89 |
90 | 代表产品:Spanner,CockroachDB
91 |
92 | ## Transparent Sharding Middleware
93 |
94 | 中间件负责的事情如下:
95 |
96 | 1. 对查询请求做路由
97 | 2. 分布式事务的协调者
98 | 3. 数据分布,数据多副本控制,数据分区
99 |
100 | 往往在各个数据库节点,需要装代理与中间件沟通,负责如下事情:
101 |
102 | 1. 在本地节点执行中间节点发来的情况,并且返回结果
103 |
104 | 优点:
105 |
106 | 1. 应用通常不需要做变化
107 |
108 | 缺点:
109 |
110 | 1. 各个节点还是运行传统数据库,即以磁盘为核心的数据库,对现有的大内存,多核服务器难以高效地利用
111 | 2. 重复的查询计划和查询优化,在中间件做一次,在各个DBMS做一次
112 |
113 | 备注:有研究表明,以磁盘为主要存储的传统DBMS,很难有效地利用非常多的核,以及更大的内存容量。
114 |
115 | 代表产品: MariaDB MaxScale, ScaleArc
116 |
117 | ## Database-as-a-Service
118 |
119 | 特点:
120 |
121 | 1. 用户可以按需使用
122 | 2. 数据库本身可能使用云产品,例如云存储等,可以较容易的实现可扩展性
123 |
124 | 代表产品:
125 |
126 | 1. Amazon Aurora
127 | 2. ClearDB
128 |
129 | # NewSQL的技术挑战有哪些?
130 |
131 | ## Main Memory Storage
132 |
133 | 传统数据库都是以磁盘为存储中心的架构,读盘操作相对较慢,一般是内存中缓存页。
134 |
135 | 现在来讲,内存较便宜,容量大,能存储大量的数据。这些纯内存操作带来的好处是,读取和写入数据速度较快。
136 |
137 | 现有的大内存服务器,对数据库对内存的管理提出了新的要求,不再是像传统数据库那样,只是用来做页缓存,可以采用更高效地内存管理方式。
138 |
139 | ## Partitioning/Sharding
140 |
141 | 数据分区一般以某几列做hash或者range分区。
142 |
143 | 特点:
144 |
145 | - 数据库需要能在多个分区执行SQL,并且合并数据结果的功能。
146 | - 把同一个用户的数据可以放在一起,即使是不同数据表的数据,可以减少通信开销。
147 | - 可以在线的添加或者删除机器。
148 | - 可以在线的迁移或复制分区。
149 |
150 | ## Concurrency Control
151 |
152 | 数据库通过Concurrency Control来提供ACID中的Atomicity和Isolation。
153 |
154 | ### Atomicity
155 |
156 | 分布式场景下,一般采用类2PC的协议,根据事务是否需要中心节点,分为以下两类:
157 |
158 | 1. 中心节点:单点,容量限制
159 | 2. 非中心节点:需要时钟的同步
160 |
161 | 关于时钟同步,不同数据库也有不同的做法,Spanner和CroachDB在时钟同步上的不同选择:
162 |
163 | ```
164 | But what makes Spanner differ- ent is that it uses hardware devices (e.g., GPS, atomic clocks) for high-precision clock synchronization. The DBMS uses these clocks to assign timestamps to transactions to enforce consistent views of its multi-version database over wide-area networks. CockroachDB also purports to provide the same kind of consistency for transactions across data centers as Span- ner but without the use of atomic clocks. They instead rely on a hybrid clock protocol that combines loosely synchronized hardware clocks and logical counters [41].
165 | ```
166 |
167 | ### Isolation
168 |
169 | 现有实现Isolation的技术主要包括:
170 |
171 | - 2PL:two phase locking
172 | - MVCC: Multiversion Concurrency Control
173 | - OCC: Optimistic Concurrency Control
174 |
175 | 大部分的数据库还是在选择使用MVCC,例如CockroachDB;有些数据库使用2PL+MVCC,修改数据的时候,还是采用2PL,例如,InnoDB,Spanner
176 |
177 | ## Secondary Indexes
178 |
179 | 一般有两种实现方式:局部索引VS全局索引
180 |
181 | 局部索引:
182 |
183 | 1. 每个partition有一部分索引数据,每次修改索引,只需要修改一个节点,但查找数据需要可能涉及多个节点
184 |
185 | 全局索引:
186 |
187 | 1. 每个partition都有完整的索引数据,每次修改索引,都需要使用分布式事务,修改所有包含此索引副本的节点,查找数据只需要在一个节点
188 |
189 | ## Replication
190 |
191 | 两个需要考虑的点:
192 |
193 | 1. 如何保证一致性:Paxos和2PC(跨Partition)
194 | 2. 同步的方式:采用同步执行命令的方式,还是同步状态的方式
195 |
196 | ## Crash Recovery
197 |
198 | 如何最小化宕机时间?
199 |
200 | 采用主备切换
201 |
202 | 如何优化新加机器恢复到同步的时间?
203 |
204 | 一般手段为做checkpoint
205 |
206 | # 讨论
207 |
208 | 可扩展性是NewSQL的一个非常重要的特点,对于中间件的方式,其上需要存路由信息,其本身的可扩展性比较难以解决,个人认为,其不应该算入NewSQL。
209 |
210 | NewSQL的技术挑战除了上述提到的之外,还有如何实现多租户架构及租户之间的隔离,负载均衡等等问题。
211 |
212 | 从整篇论文中描述的内容可以看出,NewSQL中并没有开拓性的理论技术的创新,更多的是架构的创新,以及把现有的技术如何更好地适用于当今的服务器,适用于当前的分布式架构,使得这些技术有机的结合起来,形成高效率的整体,实现NewSQL高可用,可扩展,强一致性等需求。
213 |
214 | PS:
215 | 本博客更新会在第一时间推送到微信公众号,欢迎大家关注。
216 |
217 | 
218 |
219 | # 参考文献
220 |
221 | - [What's Really New with NewSQL?](http://db.cs.cmu.edu/papers/2016/pavlo-newsql-sigmodrec2016.pdf)
222 |
--------------------------------------------------------------------------------
/doc/paxos-principle-first.md:
--------------------------------------------------------------------------------
1 | # 引言
2 |
3 | Paxos算法由lamport大师提出,目标是解决分布式环境下数据一致性的问题。Paxos算法自发表以来以晦涩难懂著称,因此,其作者于2001年发表了一篇简化版的论文,[Paxos Made Simple](http://research.microsoft.com/en-us/um/people/lamport/pubs/paxos-simple.pdf)。虽然这篇论文比前面的充满公式证明的论文容易理解,但是,如果对于Paxos算法本身要解决的问题不够理解的话,还是会很难理解该算法。Paxos原理系列文章的目标是在充分讨论Paxos要解决的问题的前提下,深入地分析和理解Paxos的原理。本文是此系列文章第一篇,主要内容如下:
4 |
5 | - Paxos算法要解决的问题是什么?
6 | - Basic Paxos在整个Paxos算法中的地位及其原理
7 |
8 | # Paxos算法要解决的问题
9 |
10 | 首先,来描述一下Paxos要解决的问题,分布式环境下数据一致性问题。
11 |
12 | 考虑下面的环境,如下图:
13 |
14 | 
15 |
16 | 在分布式环境中,为了保证服务的高可用,需要对数据做多个副本,一般是日志的方式来实现,即图中的log sequence,当某台机器宕机后,其上的请求可以自动的转到其他的Server上,同时会新找一台机器(为了保证副本数量足够),自动地把其他活着机器的日志同步过去,然后逐步回放到State Machine中去。如果Server 1,2,3中的日志是一致的话,可以保证这些Server回放到State Machine中的数据是一致的。那么问题来了,如何保证日志的一致性呢?这正是Paxos算法解决的问题,即如图中的Consensus Module所示,它们之间需要交互,保证日志中内容是完全一致的。
17 |
18 | 进一步来看日志中的内容,如下:
19 |
20 | 
21 |
22 | 一个Log Sequence一般由多个Log Item组成,每个Log Item会包含一个Command,用于记录对应的客户端请求的命令,如图中的Add,Mov,Jmp和Set等等,每个Server会根据日志的内容和顺序,一个个的把命令回放到State Machine中。Paxos算法的目标就是为了保证每个Server上的Log Sequence中的Log Item中的Command和相对顺序完全一致,这样,在任意一台机器宕机之后,能保证可以快速地将服务切换到另外一台具有完全相同数据的Server上,从而达到高可用。
23 |
24 | # Basic Paxos
25 |
26 | ## Basic Paxos要解决的问题
27 |
28 | 整个Paxos算法是为了解决整个Log Sequence一致性的问题,一般也称为Multi Paxos。而本小节要讨论的Basic Paxos是为了确定一个不变量的取值,放到上面的Log Sequence一致性上来讲,即为了确定某一个Log Item中的Command的取值,保证多个Server一旦确认该Log Item的Command之后,其值就不会再变化。
29 |
30 | 以一个例子描述确认Log Item的取值问题,如下:
31 |
32 | 
33 |
34 | 如上图所示,每个Server从客户端接受到的请求可能不一样,例如,图中的三个Server分别接收到Add,Mov和Jmp等三个不同的请求,而对于当对于当前的Log Item来讲,只能存储一个请求,而为了保证Log的一致性,又必须要Log Item中存储的Command是一致的,因此,三个Server需要协调,最终确定此Log Item存储哪一个请求,这个确定的过程就是一轮Basic Paxos过程。
35 |
36 | 最后,以比较正式的方式来定义此问题:
37 |
38 | 假设一组进程能提出(propose)value,分布式一致性算法能保证最终只有一个value被选择(chosen)。如果没有value被提出,那么就没有value被选择。如果一个value被选择了,那么这组进程能学习(learn)到被选择的value。
39 |
40 | 总体看来,一致性算法的需求如下:
41 |
42 | - 被提出的value中,只有一个value被选择
43 | - 进程在value被选择前,不应该能学习到该value
44 |
45 | 根据上面的问题,算法中总共有三类角色,即proposers,acceptors和learners,实际的实现中,一个进程可能承担三种角色中的一个或多个。这些角色之间通过发送消息的方式来相互通信,并且是在非拜占庭场景下:
46 |
47 | - 角色的计算速度可能不同,甚至可能因为宕机而终止运行,随后又被重启。在没有记录之前已选择的value的情况下,之前选择的value会丢失,因此,需要记录之前已选择的value
48 | - 消息可以时延很长,可以重复,可以丢失,但是其内容不能被篡改
49 |
50 | 在了解了上述需求,问题定义及场景后,接下来一步步地推导出Basic Paxos的最终算法。
51 |
52 | ## 如何解决问题?
53 |
54 | 整个问题分为两块:
55 |
56 | - 如何选择value?
57 | - 如何学习value?
58 |
59 | ### 如何选择value?
60 |
61 | 首先,来看一个最简单的方案,如下:
62 |
63 | 
64 |
65 | 只有一个acceptor,这个acceptor只认第一个Proposer给它提出的value,例如,在上图中,如果Proposer1先把value提给acceptor,那么acceptor最终会选择该value,即Add。
66 |
67 | 此方案的优点是简单易懂,但当acceptor挂掉并无法恢复之后,被选择的value也跟着丢失了,不满足需求。
68 |
69 | 因此,接下来的方案中,只考虑多个acceptor的场景。
70 |
71 | 为了保证仅有一个value被选择,需要在多数派的Server接受该value时,才认为该value被选择。因为任意两个多数派的acceptor集合中,必然有一个acceptor是相同的。举个例子,如果不是必须多数派的话,可能出现的场景时,有前1/3的acceptor选择value1,中间1/3的acceptor选择value2,最后1/3的acceptor选择value3,这样就会导致不止一个value被选择,不符合要求。
72 |
73 | 因为消息是有可能丢失的,因此,当只有一个value被提出的时候,acceptor应该接受它,即
74 |
75 | > P1. acceptor必须要接受它接受到的第一个value
76 |
77 | 但这会导致如下问题:
78 |
79 | 
80 |
81 | 假设Proposer 1,2,3分别提出Add,Mov和Jmp,且Proposeri{i=1,2,3}首先提给Accepti{i=1,2,3},这样会导致最终每个acceptor都接受(accept)了不同的值,最终没有value被选择。
82 |
83 | P1和某个value只有被多数派的acceptor接受后的条件表明,每个acceptor需要能接受多个value,因此,需要通过某种方法来区分,这里采用假设每个proposer提出的value都被分配了一个id,id为自然数,每个(id,value)组合称为一个proposal(提案)。每个proposal的id都是不同的。此时,如果一个value被选择,会对应于一个或多个proposal被多数派的acceptor接受,且该proposal的value对应于被选择的value。
84 |
85 | 由于允许多个proposal被选择,因此,需要保证每个被acceptor接受的proposal的value都相同,故有如下推论
86 |
87 | > P2. 如果一个proposal(id1, v1)被选择,那么,每个id大于id1的,被接受的proposal的value都等于v
88 |
89 | 由于proposal被选择,至少需要一个acceptor接受,因此,可以由P2进一步地加强约束到
90 |
91 | > P2a. 如果一个proposal(id1, v1)被选择,那么,每个id大于id1的proposal(id,value),如果被任意一个acceptor接受的话,value=v1
92 |
93 | 但P2a会存在如下问题:
94 |
95 | 
96 |
97 | 考虑以上场景,Proposal1的(10, Add)proposal被三个acceptor接受,但是,Proposer1和acceptor4之间网络不联通,导致acceptor4一致为接受任何的proposal。此时,有一个新的proposal2加入,并且能和acceptor4联通,并且,其提出的proposal为(11, Jmp),根据P1原则,acceptor4必须要接受第一个接收到的proposal,即(11, Jmp),导致其和P2a冲突。
98 |
99 | 因此,进一步增强P2a的约束为
100 |
101 | > P2b. 如果一个proposal(id1, v1)被选择,那么任意一个id大于id1的proposal的value等于v1
102 |
103 | 根据P2b,如果一个proposal(m,v)被选择,那么对于n>m的proposal的value也必须是v。假如当前最大的proposal的id为x-1,那么必定会存在一个多数派的acceptor组合C,使得每个acceptor接受的proposal都的id都属于[m,x-1],且都拥有值v,并且,每个id属于[m,x-1]的proposal,如果其被任意的acceptor接受,其value必为v。
104 |
105 | 继续对P2b加强约束,由于任意的多数的集合S,至少包含C中的一个acceptor,而每个这样的集合中,id最大的proposal的value肯定是已经选择的value,因为,P2b保证了在有proposal(m,v)被选择后,其后id大于m的proposal的value肯定是v,因此,所有acceptor中只可能处于id小于m的proposal的value不等于v,所有id大于或或等于m的proposal其value必定是v。
106 | 从而,我们可以得出,任意一个proposal(x,v),至少满足以下条件之一:
107 |
108 | > P2c. 存在多数派的acceptor集合S,对于任意的proposal(x,v),需要满足以下条件之一:
109 | > 1. S中的任意一个acceptor都没有接受过id小于x的proposal
110 | > 2. S中的acceptor接受了id处于[0...x-1]的proposal,其中,v是当中id最大的proposal的value
111 |
112 | 因此,对于一个新提出的proposal,其必须要先学习到已经被或者将要被accept的id最大的value。要预测将要被accept的proposal是很困难的,但是,我们可以在acceptor中加限制,即,如果acceptor已经接受过(n,v)了,那么任何的id小于n的proposal都不会被接受,这样就能保证当前获取到的最大的id是正确的,举个例子说明:
113 |
114 | 
115 |
116 | 上述例子发生的场景如下:
117 |
118 | 1. Proposal2在Acceptor2还没有accept (1,Jmp)的时候,学习到当前最大的proposal的value是(0,Add),因此,提出了Proposal2(2,Add)
119 | 2. Proposal3在Acceptor3接受了(1, Jmp)的时候,学习到当前最大id的Proposal的value是(1,Jmp),因此,提出了Proposal3(3,Jmp)
120 | 3. 由于网络或其他原因,Proposal3先到达Acceptor3,于是,acceptor3接受Proposal3,此时,多数派的已经达成,Jmp被选择
121 |
122 | 而后,Proposal2达到acceptor3,如果acceptor3选择接受它,那么,会出现以下情况:
123 |
124 | 
125 |
126 | Proposal2覆盖了Acceptor3已经接受过的值,导致Add成为新的多数派而被选择,不符合要求。实际上,在Proposal3提出时,由于Proposal2并没有被接受,导致,Proposal3只能学习到(1,Jmp),从这个角度来讲,Proposal2是属于Proposal3提出后被确认的,因此,需要在acceptor加以限制,不再接受比其接受过的proposal id小的proposal。
127 |
128 | 由上面的讨论,对于一个proposer需要经历如下两个步骤:
129 |
130 | - 对于一个proposer,选择一个新的proposer id,假设为n,然后发送请求到一些acceptor组合(保证大于多数派的数量),acceptor的回应保证如下:
131 | - 不再接受proposal id小于n的请求
132 | - 已接受的最大的id的proposal的value
133 |
134 | 第一步称为Prepare
135 |
136 | - 如果proposer接收到多数派的acceptor的回应,那么,它可以提出该proposal,id为n,value为第一步的value,或者是如果第一步的获得的value为空的话,proposer则使用自己提出的value,然后把该proposal发给一些acceptor组合(保证大于多数派的数量)。
137 |
138 | 第二步称为Accept
139 |
140 |
141 | 对于acceptor来讲,Prepare时,它都可以回应,但是,对于accept的时候,需要满足如下条件:
142 |
143 | > P1a. 一个acceptor在它没有回应一个比n大的prepare请求时,其可以接受id为n的proposal的value
144 |
145 | 值得注意的是,当一个acceptor已经回应了比n大的prepare请求时,就没必要回应小于或等于n的prepare请求了,因为后者肯定不会被accept了。因此,对于acceptor来讲,需要记录最大的prepare的proposal id,为了防止acceptor宕机后重启的情况,故最大的proposer的id需要被持久化存储。
146 |
147 | 用伪代码表达proposer的算法如下:
148 |
149 | ```
150 | Prepare()
151 | {
152 | select a proposal id n;
153 | send a prepare request with id n to a majority of acceptors
154 | }
155 |
156 | Accept()
157 | {
158 | if get responce from majority {
159 | send proposal(n, v); // v is the hightest-numbered proposal's value from reponses, or if any value if the responsed reported no proposals
160 | } else {
161 | abort()
162 | }
163 | }
164 | ```
165 |
166 | 用为代码表达acceptor的算法如下:
167 |
168 | ```
169 | Prepare()
170 | {
171 | if receive prepare request id with n > max_prepared_id recoreded {
172 | send(not accept proposal numbered less than n);
173 | send(highest-numbered proposal it has accepted if exist)
174 | max_prepared_id = id
175 | } else {
176 | do nothing;
177 | }
178 | }
179 |
180 | Accept()
181 | {
182 | if max_prepared_id == request_accept_id {
183 | accept the proposal
184 | } else {
185 | do nothing
186 | }
187 | }
188 | ```
189 |
190 | ### 如何学习value?
191 |
192 | 最简单粗暴的方案是,每当一个acceptor接受了新的proposal的时候,就广播给所有的learner,假设acceptor的数量为m,learner的数量为n,那么需要O(m*n)通信的开销。
193 |
194 | 为了减少通信开销,可以选出一个learner,负责接收acceptor的消息,然后再由它通知给其他的learner,这时需要O(m+n)。这个方法的缺点是如果这个learner宕机了,整个系统就无法正常工作了。改进的方案是,选择一组learner,假设数量为c,负责接受acceptor的消息,这些leaner负责通知其他的leaner,该方案的通信开销为c*o(m+n),且可用性比较高,只要c个learner中没有全部宕机,系统就可以正常工作。
195 |
196 | ## 总结
197 |
198 | 本文分析和讨论了Paxos算法要解决的问题,即分布式系统中数据一致性的问题。为了实现数据一致性,需要保证各个副本的日志序列的一致性,而日志序列是由一个个的日志项组成的,Basic Paxos算法的目标是为了解决单个日志项的一致性。直观的来看,日志序列的一致性可以用多轮的Basic Paxos来达到,但是,往往出于性能,算法稳定性等原因的考虑,需要对多轮的Basic Paxos做优化,这就是接下来要讨论的Multi Paxos算法,敬请期待。
199 |
200 | PS:
201 | 本博客更新会在第一时间推送到微信公众号,欢迎大家关注。
202 |
203 | 
204 |
205 | # 参考文献
206 |
207 | - [Paxos Made Simple](http://research.microsoft.com/en-us/um/people/lamport/pubs/paxos-simple.pdf)
208 |
--------------------------------------------------------------------------------
/doc/raft_one.md:
--------------------------------------------------------------------------------
1 | # Introduction
2 |
3 | 本文收录在[paper](https://github.com/Charles0429/papers)项目中,papers项目旨在学习和总结分布式系统相关的论文;同时本文也是[DSTORE](https://github.com/Charles0429/dstore)项目的必备知识,DSTORE的目标是自己动手实现一个分布式KV存储引擎。
4 |
5 | 本文为raft系列文章第一篇,本系列其他文章为
6 |
7 | - raft原理(二):安全性和集群成员变更
8 | - raft原理(三):日志合并及客户端交互
9 | - golang实现raft(一):选举和日志同步
10 | - golang实现raft(二):集群成员变更
11 |
12 | 本文介绍了分布式一致性算法raft的选主和日志复制原理,raft算法的主要目标是为了让分布式一致性算法更易理解和用于工程实践。
13 |
14 | raft算法的主要特性为
15 |
16 | - strong leader:raft算法使用的是strong leader方式,日志只能从leader同步到follower
17 | - leader election:使用随机定时器来选主
18 | - Membership changes:采用的是两阶段更新配置信息的方式
19 |
20 | 本文的组织结构如下
21 |
22 | - Replicated state machines
23 | - Strenthens and weaks of Paxos
24 | - Understandability
25 | - Raft basics
26 | - Raft leader election
27 | - Raft log replication
28 |
29 | # Replicated State Machine
30 |
31 | 
32 |
33 | Repicated State Machine一般分为三个部分:
34 |
35 | - Log:记录一系列的指令
36 | - State Machine:把日志中提交的指令回放到状态机中
37 | - Consensus Module:分布式环境下,保证多机的日志是一致的,这样回放到状态机中的状态是一致的
38 |
39 | 一致性算法作用于Consensus Module,一般有以下特性:
40 |
41 | - safety:在非拜占庭问题下(网络延时,网络分区,丢包,重复发包以及包乱序等),结果是正确的
42 | - availability:在半数以上机器能正常工作时,则系统可用
43 | - timing-unindependent:不依赖于时钟来保证日志一致性,错误的时钟以及极端的消息时延最多会造成可用性问题
44 |
45 | # What's wrong with Paxos?
46 |
47 | Paxos算法存在的主要问题为
48 |
49 | - 难以理解:对于大部分人来讲,难以理解Paxos论文
50 | - 难以实现:Paxos算法在工程上实现难度高,论文中缺少必要的细节,并且整个Paxos的算法的思想决定了其不易于实现
51 |
52 | # Understandability
53 |
54 | 鉴于Paxos难以理解和实现,raft的首要目标是使其易于理解,为此raft采用了以下设计思想来达到此目标:
55 |
56 | - decomposition:把整个算法分为election,log replication,safety and membership changes
57 | - Simplify the state space:例如,日志不允许有空洞等等
58 |
59 | # Raft basics
60 |
61 | Raft通过选出一个leader来简化日志副本的管理,例如,日志项(log entry)只允许从leader流向follower。
62 |
63 | 基于leader的方法,Raft算法可以分解成三个子问题:
64 |
65 | - Leader election:原来的leader挂掉后,必须选出一个新的leader
66 | - Log replication:leader从客户端接收日志,并复制到整个集群中
67 | - Safety:如果有任意的server将日志项回放到状态机中了,那么其他的server只会回放相同的日志项
68 |
69 | ## Raft Server States
70 |
71 | 一个Raft集群通常有几台Raft Server组成,每个Server处于以下三种状态之一:
72 |
73 | - leader:处理所有的客户端请求
74 | - follower:响应来自leader和candidate的请求
75 | - candidate:用于选主
76 |
77 | ## Raft Server State Transformation
78 |
79 | Raft可能的状态变化如下图:
80 |
81 | 
82 |
83 | Raft将时间分为多个term,term以连续的整数来标识,每个term以一次election开始,如果有server被选为leader,则该term的剩余时间该server都是leader。
84 |
85 | 
86 |
87 | 有些term里,可能并没有选出leader,这时候会开启一个新term来继续选主,如上图中的t3。
88 |
89 | 每个server都维护着一个当前term(current term),有可能会存在某些server整个term都没参与的情况,这时候,在server通信的时候,会带上彼此的当前term信息,server会更新成它们之间的较大值。当leader或candidate发现它们的term属于老的值时,它们会转成follower状态。
90 |
91 | Raft Server之间的通信通过RPC来进行,基础的raft算法只需要实现`RequestVote`和`AppendEntries`两个RPC。
92 |
93 | # Raft Leader Election
94 |
95 | Raft使用心跳来触发选主,当server启动时,状态是follower。当server从leader或者candidate接收到合法的RPC时,它会保持在follower状态。leader会发送周期性的心跳来表明自己是leader。
96 |
97 | 当一个follower在election timeout时间内没有接收到通信,那么它会开始选主。
98 |
99 | 选主的步骤如下:
100 |
101 | - 增加current term
102 | - 转成candidate状态
103 | - 选自己为主,然后把选主RPC并行地发送给其他的server
104 | - candidate状态会继续保持,直到下述三种情况出现
105 |
106 | candidate会在下述三种情况下退出
107 |
108 | - server本身成为leader
109 | - 其他的server选为leader
110 | - 一段时间后,没有server成为leader
111 |
112 | ## server本身被选为leader
113 |
114 | 当server得到集群中大多数的server的选举后,它会成为leader。对于每个server来讲,只能选举一台server为leader,从而使得大多数原则能确保只有一个candidate会被选成leader。
115 |
116 | 当candidate成为leader后,会发送心跳信息告诉其他server,从而防止新的选举。
117 |
118 | ## 其他server选为leader
119 |
120 | 如果在等待选举期间,candidate接收到其他server要成为leader的RPC,分两种情况处理:
121 |
122 | - 如果leader的term大于或等于自身的term,那么改candidate会转成follower状态
123 | - 如果leader的term小于自身的term,那么会拒绝该leader,并继续保持candidate状态
124 |
125 | ## 一段时间后,没有server成为leader
126 |
127 | 有可能,很多follower同时变成candidate,导致没有candidate能获得大多数的选举,从而导致无法选出主。当这个情况发生时,每个candidate会超时,然后重新发增加term,发起新一轮选举RPC。需要注意的是,如果没有特别处理,可能出导致无限地重复选主的情况。
128 |
129 | Raft采用随机定时器的方法来避免上述情况,每个candidate选择一个时间间隔内的随机值,例如150-300ms,采用这种机制,一般只有一个server会进入candidate状态,然后获得大多数server的选举,最后成为主。每个candidate在收到leader的心跳信息后会重启定时器,从而避免在leader正常工作时,会发生选举的情况。
130 |
131 | # Raft Log replication
132 |
133 | 当选出leader后,它会开始接受客户端请求,每个请求会带有一个指令,可以被回放到状态机中。leader把指令追加成一个log entry,然后通过`AppendEntries` RPC并行的发送给其他的server,当改entry被多数派server复制后,leader会把该entry回放到状态机中,然后把结果返回给客户端。
134 |
135 | 当follower宕机或者运行较慢时,leader会无限地重发`AppendEntries`给这些follower,直到所有的follower都复制了该log entry。
136 |
137 | 
138 |
139 | log按照上图方式组织,每个log entry存储了指令和term信息,由leader指定。每个log entry有个数字索引(index)来表名其在log中的位置。
140 |
141 | leader决定什么时候将一个log entry回放到状态机中是安全的,被回放的log entry称为committed,raft保证所有committed log entry会被持久化,并且最终会被回放到所有可工作的状态机中。
142 |
143 | 一个log在大多数的server已经复制它之后,则是committed(这个特指在leader的term里面的日志),在复制该log的同时,同时也会告诉已复制该log entry的follower,其之前的log entry也被提交了,follower则可以回放其之前的log entry。例如上图中的entry 7。leader会维护最大的committed的entry的index,当一个follower发现log entry已提交,则会将它回放到状态机中。
144 |
145 | raft的log replication保证以下性质(Log Matching Property):
146 |
147 | - 如果两个log entry有相同的index和term,那么它们存储相同的指令
148 | - 如果两个log entry在两份不同的日志中,并且有相同的index和term,那么它们之前的log entry是完全相同的
149 |
150 | 其中特性一通过以下保证:
151 |
152 | - leader在一个特定的term和index下,只会创建一个log entry
153 | - log entry不会改变它们在日志中的位置
154 |
155 | 特性二通过以下保证:
156 |
157 | `AppendEntries`会做log entry的一致性检查,当发送一个`AppendEntries`RPC时,leader会带上需要复制的log entry前一个log entry的(index, iterm)
158 |
159 | - 如果follower没有发现与它一样的log entry,那么它会拒绝接受新的log entry
160 |
161 | 这样就能保证特性二得以满足。
162 |
163 | 在正常情况下,leader和follower会保持一致,一致性检查通常都会成功。但是,当leader崩溃后,可能会出现日志不一致的情况,通过一个例子来说明。
164 |
165 | 
166 |
167 | 如上图所示,raft的leader强制以自己的日志来复制不一致的日志,通过以下方法:
168 |
169 | - 找到leader和follower最后一个相同的log entry,然后删掉follower后面的日志,然后把该log entry之后的leader日志复制给follower
170 |
171 | 上述方法是通过`AppendEntries`的一致性检查实现的,如下:
172 |
173 | - leader为每个follower维护一个*nextIndex*,表明下一个将要发送给follower的log entry
174 | - 当leader刚上任时,会把所有的*nextIndex*设置成其最后一个log entry的index加1,如上图,则是11
175 | - 当follower的日志和leader不一致时,一致性检查会失败,那么会把*nextIndex*减1
176 | - 最终*nextIndex*会是leader和follower相同log entry的index加1,这时候,再发送`AppendEntries`会成功,并且会把follower的所有之后不一致的日志删除掉
177 |
178 | **优化**
179 |
180 | 上述一次回退一个log entry的方法效率较低,在发生冲突时,可以让follower把冲突的term的第一个日志的index发回给leader,这样leader就可以一次过滤掉该term的所有log entry。
181 |
182 | 在正常情况下,log entry可以通过一轮RPC就能将日志复制到大多数的server,少数的慢follower不会影响性能。
183 |
184 | PS:
185 | 本博客更新会在第一时间推送到微信公众号,欢迎大家关注。
186 |
187 | 
188 |
189 | # 参考文献
190 |
191 | - [In Search of an Understandable Consensus Algorithm(Extended Version)](https://pdos.csail.mit.edu/6.824/papers/raft-extended.pdf)
192 |
--------------------------------------------------------------------------------
/doc/raft_three.md:
--------------------------------------------------------------------------------
1 | # Introduction
2 |
3 | 本文收录在[paper](https://github.com/Charles0429/papers)项目中,papers项目旨在学习和总结分布式系统相关的论文;同时本文也是[DSTORE](https://github.com/Charles0429/dstore)项目的必备知识,DSTORE的目标是自己动手实现一个分布式KV存储引擎。
4 |
5 | 本文为raft系列文章第三篇,本系列其他文章为
6 |
7 | - [raft原理(一):选举和日志复制](http://oserror.com/distributed/raft-principle-one/)
8 | - [raft原理(二):安全性和集群成员变更](http://oserror.com/distributed/raft-principle-two/)
9 | - golang实现raft(一):选举和日志同步
10 | - golang实现raft(二):集群成员变更
11 |
12 | 本文的组织结构如下
13 |
14 | - Log compaction
15 | - Client interaction
16 | - Evaluation
17 |
18 | # Log compaction
19 |
20 | Raft的日志会随着处理客户端请求数量的增多而不断增大,在实际系统中,日志不可能会无限地增长,原因如下:
21 |
22 | - 占用的存储空间随着日志增多而增加
23 | - 日志越多,server当掉重启时需要回放的时间就越长
24 |
25 | 因此,需要定期地清理日志,Raft采用最简单的快照方法。对系统当前做快照时,会把当前状态持久化到存储中,然后到快照点的日志项都可以被删除。
26 |
27 | 
28 |
29 | Raft算法中每个server单独地做快照,即把当前状态机的状态写入到存储中(状态机中的状态都是已提交的log entry回放出来的)。除了状态机的状态外,Raft快照中还需要一些元数据信息,包括如下:
30 |
31 | - 快照中包含的最后一个log entry的index和term,记录这些信息的目的是为了使得`AppendEntries`RPC的一致性检查能通过,因为,在复制紧跟着快照后的log entry时,`AppendEntries` RPC带上需要复制的log entry前一个log entry的(index, iterm),即快照的最后一个log entry的(index,term),因此,快照中需要记录最后一个log entry的(index,term)
32 | - 为了支持集群成员变更,快照中保存的元数据还会存储集群最新的配置信息。
33 |
34 | 当server完成快照后,可以删除快照最后一个log entry及其之前所有的log entry,以及之前的快照。
35 |
36 | 虽然每个server是独立地做快照的,但是也有可能存在需要leader向follower发送整个快照的情况,例如,一个follower的日志处于leader的最近一次快照之前,恰好leader做完快照之后把其快照中的log entry都删除了,这时,leader就无法通过发送log entry来同步了,只能通过发送完整快照。
37 |
38 | leader通过`InstallSnapshot` RPC来完成发送快照的功能,follower收到此RPC后,根据不同情况会有不同的处理:
39 |
40 | **当follower中缺失快照中的日志时**
41 |
42 | - follower会删除掉其上所有日志,并清空状态机
43 |
44 | **当follower中拥有快照中所有的日志时**
45 |
46 | - follower会删掉快照所覆盖的log entry,但快照后所有日志都保留。备注:这里论文中没有提是否还是从leader接受快照,个人觉得follower可以自己做快照,并拒绝掉leader发快照的RPC请求
47 |
48 | 对于Raft快照,关于性能需要考虑的点有:
49 |
50 | - server何时做快照,太频繁地做快照会浪费磁盘I/O;太不频繁会导致server当掉后回放时间增加,可能的方案为当日志大小到一定空间时,开始快照。备注:如果所有server做快照的阈值空间都是一样的,那么快照点也不一定相同,因为,当server检测到日志超过大小,到其真正开始做快照中间还存在时间间隔,每个server的间隔可能不一样
51 | - 写快照花费的时间很长,不能让其影响正常的操作。可以采用copy-on-write操作,例如linux的fork
52 |
53 | # Client Interaction
54 |
55 | Raft的client会把所有的请求发到leader上执行,在client刚启动时,会随机选择集群中的一个server
56 |
57 | - 如果选择的server是leader,那么client会把请求发到该server上
58 | - 如果选择的server不是leader,该server会把leader的地址告诉给client,后续client会把请求发给该leader
59 | - 如果此时没有leader,那么client会timeout,client会重试其他server,直到找到leader
60 |
61 | Raft的目标使得client是linerizable的,即每个操作几乎是瞬间的,在其调用到返回结果的某个时间点,执行其执行一次。由于需要client的请求正好执行一次,这就需要client的配合,当leader挂掉之后,client需要重试其请求,因为有可能leader挂掉之前请求还没有成功执行。但是,也有可能leader挂掉之前,client的请求已经执行完成了,这时候就需要新leader能识别出该请求已经执行过,并返回之前执行的结果。可以通过为client的每个请求分配唯一的编号,当leader检测到请求没有执行过时,则执行它;如果执行过,则返回之前的结果。
62 |
63 | 只读的请求可以不写log就能执行,但是它有可能返回过期的数据,有如下场景:
64 |
65 | - 老的leader挂掉了,但它自身还认为自己是leader,于是client发读请求到该server时,可能获得的是老数据
66 |
67 | Raft通过如下方法避免上述问题:
68 |
69 | - leader需要自己知道哪些log entry是已经提交的,在正常情况下,leader一直是有已提交过的log entry的,但是,在leader刚当选的时候,需要当场获取,可以通过提交一个空的log entry来获取已提交过的log entry(备注:个人理解是为了避免commiting from previous leader那种情况)
70 | - leader在执行只读请求时,需要确定自己是否还是leader,通过和大多数的server发送heartbeat消息,来确定自己是leader,然后再决定是否执行该请求
71 |
72 | # Evaluation
73 |
74 | Raft的性能测试配置如下:
75 |
76 | - broadcast time = 15ms
77 | - 5 servers
78 | - 图中的时间范围是electionTimeout的随机范围
79 |
80 | 
81 |
82 | 从上图的第一幅图,可以看出:
83 |
84 | - 5ms的随机范围,可以将downtime减少到平均283ms
85 | - 50ms的随机范围,可以将最坏的downtime减少到513ms
86 |
87 | 从第二幅图,可以看出:
88 |
89 | - 可以通过减少electionTimeout来减少downtime
90 | - 过小的electionTimeout可能会造成leader的心跳没有发给其他server前,其他server就开始选举了,造成不必要的leader的切换,一般建议范围为[150ms-300ms]
91 |
92 | PS:
93 | 本博客更新会在第一时间推送到微信公众号,欢迎大家关注。
94 |
95 | 
96 |
97 | # 参考文献
98 |
99 | - [In Search of an Understandable Consensus Algorithm(Extended Version)](https://pdos.csail.mit.edu/6.824/papers/raft-extended.pdf)
100 |
101 |
--------------------------------------------------------------------------------
/doc/raft_two.md:
--------------------------------------------------------------------------------
1 | # Introduction
2 |
3 | 本文收录在[paper](https://github.com/Charles0429/papers)项目中,papers项目旨在学习和总结分布式系统相关的论文;同时本文也是[DSTORE](https://github.com/Charles0429/dstore)项目的必备知识,DSTORE的目标是自己动手实现一个分布式KV存储引擎。
4 |
5 | 本文为raft系列文章第二篇,本系列其他文章为
6 |
7 | - [raft原理(一):选举和日志复制](http://oserror.com/distributed/raft-principle-one/)
8 | - raft原理(三):日志合并及客户端交互
9 | - golang实现raft(一):选举和日志同步
10 | - golang实现raft(二):集群成员变更
11 |
12 | 本文将继续讨论raft原理,包括raft的安全性和集群成员变更,全文组织结构如下
13 |
14 | - Safety
15 | - Cluster Membership Changes
16 |
17 | # Safety
18 |
19 | 前面描述了raft是如何选主和复制日志的,但是没有讨论raft是如何保证所有server的状态机按照相同的顺序执行完全相同的指令的。本节将在server被选为主的限制进行补充,保证了任何term被选为leader都会包含前面所有提交过的log entry,具体地将会通过本节描述的一系列规则来阐述。
20 |
21 | ## Election Restriction
22 |
23 | 在一些一致性算法中,即使一台server没有包含所有之前已提交的log entry,也能被选为主,这些算法需要把leader上缺失的日志从其他的server拷贝到leader上,这种方法会导致额外的复杂度。相对而言,raft使用一种更简单的方法,即它保证所有已提交的log entry都会在当前选举的leader上,因此,在raft算法中,日志只会从leader流向follower。
24 |
25 | 为了实现上述目标,raft在选举中会保证,一个candidate只有得到大多数的server的选票之后,才能被选为主。得到大多数的选票表明,选举它的server中至少有一个server是拥有所有已经提交的log entry的,而leader的日志至少和follower的一样新,这样就保证了leader肯定有所有已提交的log entry。
26 |
27 | ## Committing entries from previous terms
28 |
29 | 从日志复制一节可以知道,**在当前term**,一个leader知道一个log entry在复制到大多数server后,其就可以被提交了。当一个leader在提交log entry之前宕机掉,后面选举出来的leader会复制该log entry,但是,一个leader不能立马对之前term的log entry是否复制到大多数server来判断其是否已被提交。
30 |
31 | 
32 |
33 | 如上图的例子,图(c)就发生了一个log entry虽然已经复制到大多数的server,但是仍然有可能被覆盖掉的可能,如图(d),整个发生的时序如下:
34 |
35 | - 图a中,S1被选为主,然后复制到log index为2的log entry到S2上
36 | - 图b中,S1挂掉,然后S5获得了S3,S4和自身的选举,成为leader,然后,其从客户端收到了一个新的log entry(3)
37 | - 图c中,S5挂掉,S1重新正常工作,又被选为主,继续复制log entry(2),在log entry(2)被提交前,S1又挂掉
38 | - 图d中,S5又重新被选为leader,然后,会把term 3的log entry覆盖到其他log index为2的log entry
39 |
40 | 因此,在raft中,不会通过日志复制的个数来提交之前term的log entry,只有当前term的log entry才会通过日志副本的个数来判断,例如,图e中,如果S1在挂掉前把log entry(4)复制到了大多数的server后,就能保证之前的log entry(2)被提交了,之后S5也就不可能被选为leader了。
41 |
42 | ## Safety argument
43 |
44 | 本小节将证明已经被leader提交的log entry,在之后选举出的leader中也会存在。
45 |
46 | 以反证法来证明,假设Term T的leader T提交了一个log entry,但是此log entry没有在之后的某些term中,不妨设最小的Term U的leader U中不存在此log entry。证明如下:
47 |
48 | 
49 |
50 | 1. 提交的log entry在leader U被选为主之前已经不存在了,因为leader不会删除或覆盖自己之前的log entry;
51 | 2. leader T复制该log entry到大多数的server上,并且leader U获得了大多数server的选举,因此,至少有一个server(称为voter)同时复制了该log entry,并且选举U为leader,例如上图中的S3就是这样的server;
52 | 3. voter在选举leader U之前,从leader T获得了此log entry,因为U > T,如果先选举leader U的话,则S1在term T发送给它的`AppendEntries`RPC会失败;
53 | 4. voter在选举leader U的时候,还存储着此log entry,因为,假设中U是最小的不存在此log entry的leader,且[T,U)之间的leader不会删除和覆盖自己的log entry且follower只会删除和leader冲突的log entry;
54 | 5. voter选举U为leader,说明U的日志至少是和voter一样新的,这点导致两点假设不成立;
55 | 6. 第一,如果voter和U的最后的term相同,那么leader U则至少在最后的term的日志和voter的一样长,即包含了leader T提交的log entry;
56 | 7. 第二,如果voter和U的最后的term不相同,那么U的必定大于voter的,则leader U必须包含term T的所有日志,因为U > T;
57 | 8. 因此,假设不成立,不可能存在term U,使得term U中的leader不包含之前leader已经提交过的log entry。
58 |
59 | ## Follower and candidate crashes
60 |
61 | follower崩溃掉后,会按如下处理
62 |
63 | - leader会不断给它发送选举和追加日志的RPC,直到成功
64 | - follower会忽略它已经处理过的追加日志的RPC
65 |
66 | ## Time and availability
67 |
68 | 在raft中,election timeout的值需要满足如下条件:
69 |
70 | ```
71 | broadcastTime << electionTimeout << MTBF
72 | ```
73 |
74 | 其中broadcastTimeout是server并行发送给其他server RPC并收到回复的时间;electionTimeout是选举超时时间;MTBF是一台server两次故障的间隔时间。
75 |
76 | electionTimeout要大于broadcastTimeout的原因是,防止follower因为还没收到leader的心跳,而重新选主。
77 |
78 | electionTimeout要小于MTBF的原因是,防止选举时,能正常工作的server没有达到大多数。
79 |
80 | 对于boradcastTimeout,一般在[0.5ms,20ms]之间,而MTBF一般非常大,至少是按照月为单位。因此,一般electionTimeout一般选择范围为[10ms,500ms]。因此,当leader挂掉后,能在较短时间内重新选主。
81 |
82 | # Cluster Membership Changes
83 |
84 | 在集群server发生变化时,不能一次性的把所有的server配置信息从老的替换为新的,因为,每台server的替换进度是不一样的,可能会导致出现双主的情况,如下图:
85 |
86 | 
87 |
88 | 如上图,Server 1和Server 2可能以Cold配置选出一个主,而Server 3,Server 4和Server 5可能以Cnew选出另外一个主,导致出现双主。
89 |
90 | raft使用两阶段的过程来完成上述转换:
91 |
92 | - 第一阶段,新老配置都存在,称为joint consensus
93 | - 第二阶段,替换成新配置
94 |
95 | 
96 |
97 | - leader首先创建Cold,new的log entry,然后提交(保证大多数的old和大多数的new都接收到该log entry);
98 | - leader创建Cnew的log entry,然后提交,保证大多数的new都接收到了该log entry。
99 |
100 | 这个过程中,有几个问题需要考虑。
101 |
102 | - 新加入的server一开始没有存储任何的log entry,当它们加入到集群中,可能有很长一段时间在追加日志的过程中,导致配置变更的log entry一直无法提交
103 |
104 | Raft为此新增了一个阶段,此阶段新的server不作为选举的server,但是会从leader接受日志,当新加的server追上leader时,才开始做配置变更。
105 |
106 | - 原来的主可能不在新的配置中
107 |
108 | 在这种场景下,原来的主在提交了Cnew log entry(计算日志副本个数时,不包含自己)后,会变成follower状态。
109 |
110 | - 移除的server可能会干扰新的集群
111 |
112 | 移除的server不会受到新的leader的心跳,从而导致它们election timeout,然后重新开始选举,这会导致新的leader变成follower状态。Raft的解决方案是,当一台server接收到选举RPC时,如果此次接收到的时间跟leader发的心跳的时间间隔不超过最小的electionTimeout,则会拒绝掉此次选举。这个不会影响正常的选举过程,因为,每个server会在最小electionTimeout后发起选举,而可以避免老的server的干扰。
113 |
114 | PS:
115 | 本博客更新会在第一时间推送到微信公众号,欢迎大家关注。
116 |
117 | 
118 |
119 | # 参考文献
120 |
121 | - [In Search of an Understandable Consensus Algorithm(Extended Version)](https://pdos.csail.mit.edu/6.824/papers/raft-extended.pdf)
122 |
--------------------------------------------------------------------------------
/doc/transaction-isolation-first.md:
--------------------------------------------------------------------------------
1 | # 引言
2 |
3 | 一般的数据库教科书上都会介绍,事务有ACID四个特性,分别是atomicity, consistency, isolation和duriablity。本文主要讨论是事务的isolation特性,即隔离性。
4 |
5 | 谈到事务的隔离性,一般是指ANSI SQL标准下的四种隔离级别,即Read Uncommitted, Read Committed, Repeatable Read和Serialibility。但ANSI SQL的事务隔离级别的标准存在以下限制:
6 |
7 | - 没有提及写操作的隔离性
8 | - ANSI SQL的标准比较老,对于采用多版本并发控制实现隔离性的级别不能够很好的描述
9 |
10 | 本文主要在分析ANSI SQL标准下的事务隔离级别之后,讨论其限制,以及扩展,全文的组织架构如下:
11 |
12 | - ANSI SQL标准下的事务隔离级别
13 | - ANSI SQL标准的限制及其扩展
14 |
15 | 本文收录在[papers项目](https://github.com/Charles0429/papers),papers项目旨在学习和总结分布式系统相关的论文。
16 |
17 | # ANSI SQL标准下的事务隔离级别
18 |
19 | 在数据库中,多个事务往往是并发执行的,事务之间可能会存在干扰,从而导致数据不正确的问题。为了保证事务之间执行不互相干扰,最简单的方案则是串行的执行一个个事务,但这会降低吞吐量。为此,ANSI SQL标准引入事务隔离级别,描述了并发事务的各种干扰级别,使得应用程序可以在吞吐量和正确性上做决策,不同的事务隔离级别保证不同程度的正确性,一般而言,事务隔离级别越低,吞吐量越高,正确性越低。
20 |
21 | ANSI SQL的隔离级别主要是从解决应用程序出现的各种干扰现象中,而设计出来的,其隔离级别主要是为了解决以下三种现象:
22 |
23 | 1. 脏读 (P1)
24 | 2. 不可重复读 (P2)
25 | 3. 幻读 (P3)
26 |
27 | 首先,来看脏读P1的发生的时序:
28 |
29 | > 事务T1写了数据X,事务T2读了数据X,事务T1回滚,此时,事务T2读取到的是脏数据
30 |
31 | 其次,来看不可重复读P2发生的时序:
32 |
33 | > 事务T1读了数据X,事务T2写了数据X,事务T2提交,事务T1再次读数据X,两次读到的数据X不一样
34 |
35 | 最后,来看幻读P3发生的时序
36 |
37 | > 事务T1读了满足X>=m且X<=n的数据,事务T2插入一条数据,满足条件X>=m且x<=n,事务T2提交,事务T1再次读满足X>=m且X<=n的数据,两次读到的数据不一样
38 |
39 | 用稍微形式化的语言描述上述现象发生的时序,假设W1(X)表示事务T1修改了数据项X,而R2(X),表示事务T2读了数据项X;W1(P)表示事务T1修改了满足谓词条件P的数据项,而R2(P),表示事务T2读了满足谓词条件P的数据项。C1表示事务T1提交,A1表示事务T1回滚。
40 |
41 | 脏读P1
42 |
43 | > W1(X)...R2(X)...A1...R2(X)
44 |
45 | 不可重复读P2
46 |
47 | > R1(X)...W2(X)...C2...R1(X)
48 |
49 | 幻读P3
50 |
51 | > R1(P)...W2(P)...C2...R1(P)
52 |
53 | 针对三种现象,ANSI SQL标准设定了四种事务隔离级别,如下:
54 |
55 | 1. Read Uncommitted:有可能发生P1,P2和P3
56 | 2. Read Committed:不可能发生P1,有可能发生P2和P3
57 | 3. Repeatable Read:不可能发生P1,P2,有可能发生P3
58 | 4. Serializable:不可能发生P1,P2和P3
59 |
60 | 整个事务个隔离级别,与杜绝的现象的对应关系如下图:
61 |
62 | 
63 |
64 | 值得一提的是,在Serializable下,不可能发生P1,P2和P3,但并不表明,不发生P1,P2和P3就一定是在Serializable。真正的在Serializable是指事务并发执行下得到的结果,与各个事务串行执行下的某个结果相同。
65 |
66 | # ANSI SQL标准的限制及其扩展
67 |
68 | 如上文提到,ANSI SQL有如下限制:
69 |
70 | - 没有提及写操作的隔离性
71 | - ANSI SQL的标准比较老,对于采用多版本并发控制实现隔离性的级别不能够很好的描述
72 |
73 | 对于第一点,没有提及写操作的隔离性,有如下现象
74 |
75 | 脏写P0
76 |
77 | > 事务T1修改X,事务T2修改X,事务T1回滚,此时不知回滚到什么值
78 |
79 | 举个例子说明,假设事务T1修改X前,X=100,事务T1要把修改成10,事务T2要把X修改成20,如下:
80 |
81 | ```
82 | X=100
83 | W1X(10)
84 | W2X(20)
85 | A1
86 | ```
87 |
88 | 事务T1回滚时,如果选择回滚到X=20,那么如果事务T2再回滚时,无法回滚到最原先的100;如果回滚到X=100,那么事务T2提交时,就无法知道写入的是X=20,在这种场景下,是无解的。
89 |
90 | 因此,对于写一定是要隔离的,否则,无法保证事务的正确性。所以,对于所有的ANSI SQL标准下的隔离级别,需要增强到都满足P0不发生的级别。
91 |
92 | 对于写操作,还存在写丢失问题,发生在如下场景
93 |
94 | 写丢失P4
95 |
96 | > 事务T1读数据X,事务T2读数据X,事务T2修改数据X,事务T1修改数据X,最终写入的数据是脏数据
97 |
98 | 举个例子,假如原先X=100,事务T1对X加5,事务T2对X减1,预期的结果应该是100+5-1=104,而如果在写丢失的情况下,则如下
99 |
100 | ```
101 | T1 T2
102 | R(X=100)
103 | R(X=100)
104 | W(X=99)
105 | W(X=104)
106 | ```
107 |
108 | 如上场景,最终写入到数据库时,X=104,这属于数据不一致,是应该杜绝的。
109 |
110 | 写丢失在现象在ANSI SQL的Read Committed级别可能发生,但是Repeatable Read级别不可能发生。为此,引入Cursor Stability隔离级别,保证不会发生P4。但是,P4不会在Repeatable级别发生,因为P2禁止了事务T1读X后,事务T2写X的场景。因此,Cursor Stability的隔离级别处于Read Committed和Repeatable Read之间。
111 |
112 | 现有的工业界产品,如MySQL等,大多都会采用MVCC等多版本并发控制来达到某种隔离性,而在ANSI SQL标准中,是没有考虑多版本的,因此,有必要讨论多版本并发控制下的隔离级别与现有的ANSI SQL标准下的隔离级别的不同。
113 |
114 | 先简单的介绍下一种多版本并发控制的思路,即Basic Time Ordering,其流程如下:
115 |
116 | 1. 事务T1开始时,先申请一个时间戳,记做Start-Timestamp
117 | 2. 事务T1的读不会阻塞,因为,它会读其Start-Timestamp之前的版本,其他事务在Start-Timestamp之后的修改,对该事务是不可见的
118 | 3. 事务T1本身的修改,包括更新,插入和删除,都会保存在事务的上下文中,方便事务本身重复读取修改过的数据
119 | 4. 当事务T1要提交时,它会获取一个Commit-Timestamp,此Commit-Timestamp要保证比现有的所有其他事务的Start-Timestmap和Commit-Timestmap要大,如果存在其他事务Commit-Timestamp在事务T1的[Start-Timestamp, Commit-Timestamp]之内,且该事务修改了事务T1修改的数据,那么,事务T1会被终止,否则,事务提交
120 |
121 | 可以看出,步骤4中保证了P4写丢失现象不会在Basic Time Ordering中发生,把Basic Time Ordering达到的隔离级别称为Snapshot级别。由于会读取Start-TimeStamp之前提交的记录,因此,Snapshot肯定是满足Read Committed。
122 |
123 | 接下来,要讨论的是Snapshot和Repeatable Read之间的关系:
124 |
125 | Snapshot读取的是某个时间点前的快照,因此,也不会出现不可重复读的现象,所以,从这个角度来说Snapshot的隔离级别要大于Repeatable Read,但考虑以下场景:
126 |
127 | > R1(X)...R2(Y)...W1(Y)...W2(X)...C1...C2
128 |
129 | 这种场景在Repeatable Read级别是被禁止的,因为T2读了数据Y之后,T1修改Y并提交了,会导致不可重复读。而对于Snapshot是可能发生这种场景的,以X+Y要满足条件大于0为例,说明Snapshot下可能违反此约束,如下
130 |
131 | ```
132 | 两个事务开始之前X=1,Y=2,时间戳为10000
133 | 事务T1开始,申请时间戳为10001,R1(X)=1
134 | 事务T2开始,申请时间戳为10003,R2(Y)=2
135 | 事务T1,W1(Y)=0
136 | 事务T2,W2(X)=0
137 | 事务T1提交,发现X+Y=1+0,满足约束
138 | 事务T2提交,发现X+Y=0+2,满足约束
139 | ```
140 |
141 | 虽然事务T1和事务T2都认为满足约束,但是两个事务都执行完成后,约束不满足。
142 |
143 | 因此,在这个现象上,Snapshot的隔离级别要小于Repeatable Read。故,Snapshot的隔离级别既不大于Repeatable Read,也不小于Repeatable Read。
144 |
145 | # 总结
146 |
147 | 事务隔离级别是比较有意思的话题,现阶段也有一些技术来实现各种隔离级别,接下来的一些文章会讨论实现事务隔离级别的技术,以及工业界产品中使用的技术,如下:
148 |
149 | - Two Phase Locking
150 | - Basic Time Ordering
151 | - Multi-version Concurrentcy Control
152 | - Optimistic Concurrency Control
153 | - MySQL,Oracle,Spanner等产品是使用何种技术来做事务隔离的
154 |
155 | PS:
156 | 本博客更新会在第一时间推送到微信公众号,欢迎大家关注。
157 |
158 | 
159 |
160 | # 参考文献
161 |
162 | - [A Critique of ANSI SQL Isolation Levels](https://www.microsoft.com/en-us/research/wp-content/uploads/2016/02/tr-95-51.pdf)
163 |
--------------------------------------------------------------------------------
/image/bigtable_data_model.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Charles0429/papers/b3fde2366c4f44d6a30891b31b01f9a63f63a551/image/bigtable_data_model.png
--------------------------------------------------------------------------------
/image/bigtable_scan.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Charles0429/papers/b3fde2366c4f44d6a30891b31b01f9a63f63a551/image/bigtable_scan.png
--------------------------------------------------------------------------------
/image/bigtable_tablet_location.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Charles0429/papers/b3fde2366c4f44d6a30891b31b01f9a63f63a551/image/bigtable_tablet_location.png
--------------------------------------------------------------------------------
/image/bigtable_tablet_serving.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Charles0429/papers/b3fde2366c4f44d6a30891b31b01f9a63f63a551/image/bigtable_tablet_serving.png
--------------------------------------------------------------------------------
/image/bigtable_write.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Charles0429/papers/b3fde2366c4f44d6a30891b31b01f9a63f63a551/image/bigtable_write.png
--------------------------------------------------------------------------------
/image/chubby_session.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Charles0429/papers/b3fde2366c4f44d6a30891b31b01f9a63f63a551/image/chubby_session.png
--------------------------------------------------------------------------------
/image/chubby_structure.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Charles0429/papers/b3fde2366c4f44d6a30891b31b01f9a63f63a551/image/chubby_structure.png
--------------------------------------------------------------------------------
/image/consistency_model.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Charles0429/papers/b3fde2366c4f44d6a30891b31b01f9a63f63a551/image/consistency_model.png
--------------------------------------------------------------------------------
/image/gfs_architecture.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Charles0429/papers/b3fde2366c4f44d6a30891b31b01f9a63f63a551/image/gfs_architecture.png
--------------------------------------------------------------------------------
/image/gfs_write_control.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Charles0429/papers/b3fde2366c4f44d6a30891b31b01f9a63f63a551/image/gfs_write_control.png
--------------------------------------------------------------------------------
/image/mapreduce_overview.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Charles0429/papers/b3fde2366c4f44d6a30891b31b01f9a63f63a551/image/mapreduce_overview.png
--------------------------------------------------------------------------------