2.3 GFS的原子性
接下来我们分析GFS的一致性,首先从原子性开始分析。
2.3.1 write和record append的区别
前面讲过,如果一次写入的数据量超过了chunk的边界,那么这次写入会被分解成多个操作,write和record append在处理数据跨越边界时的行为是不同的。
下面我们举例来进行说明。
例子1:目前文件有两个chunk,分别是chunk1和chunk2。客户端1在54MB的位置写入20MB数据。同时,客户端2也在54MB的位置写入20MB的数据。两个客户端都写入成功。
前面讲过,chunk的大小是固定的64MB。客户端1的写入跨越了chunk的边界,因此要被分解成两个操作,其中第一个操作写入chunk1最后10MB数据;第二个操作写入chunk2开头10MB数据。
客户端2的写入也跨越了chunk的边界,因此也要被分解为两个操作,其中第一个操作(作为第三个操作)写入chunk1最后10MB数据;第二个操作(作为第四个操作)写入chunk2开头10MB数据。
两个客户端并发写入数据,因此第一个操作和第三个操作在chunk1上是并发执行的,第二个操作和第四个操作在chunk2上也是并发执行的。如果chunk1先执行第一个操作,后执行第三个操作;chunk2先执行第四个操作,后执行第二个操作,那么最后在chunk1上会保留客户端1写入的数据,在chunk2上会保留客户端2写入的数据。虽然客户端1和客户端2的写入都成功了,但最后的结果既不是客户端1想要的结果,也不是客户端2想要的结果,而是客户端1和客户端2写入的混合结果。对于客户端1和客户端2来说,它们的操作都不是原子的。
例子2:目前文件有两个chunk,分别是chunk1和chunk2。一个客户端在54MB的位置写入20MB数据,但这次写入失败了。
这次写入跨越了chunk的边界,因此要被分解成两个操作,其中第一个操作写入chunk1最后10MB数据;第二个操作写入chunk2开头10MB数据。chunk1执行第一个操作成功了,chunk2执行第二个操作失败了。也就是说,写入的这部分数据,一部分是成功的,一部分是失败的。这也不是原子操作。
例子3:目前文件有一个chunk,为chunk1。一个客户端在54MB的位置追加一个12MB的记录,最终写入成功。
由于这个record append操作最多能在chunk1中写入10MB数据,而要写入的数据量(12MB)超过chunk的剩余空间,剩余空间会被填充,GFS会新建一个chunk,为chunk2,这次写入操作会在chunk2上重试。这样就保证了record append操作只会在一个chunk上生效,从而避免了文件操作跨越边界被分解成多个chunk操作,也就避免了写入的数据一部分成功、一部分失败和并发写入的数据混在一起这两种非原子性的行为。
2.3.2 GFS中原子性的含义
GFS中的一次写入,可能会被分解成分布在多个chunk上的多个操作,并且由于master的锁机制和chunk lease机制,如果写入操作发生在一个chunk上,则可以保护它是原子的。但是如果一些文件写入被分解成多个chunk写入操作,那么GFS并不能保证多个chunk写入要么同时成功、要么同时失败,会出现一部分chunk写入成功、一部分chunk写入失败的情况,所以不具有原子性。之所以称record append操作是原子的,是因为GFS保证record append操作不会被分解成多个chunk写入操作。如果write操作不跨越边界,那么write操作也满足GFS的原子性。
2.3.3 GFS中多副本之间不具有原子性
GFS中一个chunk的副本之间是不具有原子性的,不具有原子性的副本复制行为表现为:一个写入操作,如果成功,那么它在所有的副本上都成功;如果失败,则有可能是一部分副本成功,而另一部分副本失败。
在这样的行为下,失败会产生以下结果:
● write在写入失败后,虽然客户端可以重试,直到写入成功,达到一致的状态,但是如果在重试成功以前,客户端出现宕机,那么就变成永久的不一致了。
● record append在写入失败后,也会重试,但是与write的重试不同,它不是在原有的offset处重试,而是在失败的记录后面重试,这样record append留下的不一致是永久的,并且还会出现重复问题。如果一条记录在一部分副本上写入是成功的,在另外一部分副本上写入是失败的,那么这次record append就会将失败的结果告知客户端,并且让客户端重试。如果重试后成功,那么在某些副本上,这条记录就会被写入两次。
从以上结果可以得出结论:record append保证至少有一次原子操作(at least once atomic)。