对强一致性模型的思考
最近在完善强一致性的存储系统的时候,发现对强一致性的理解不够透彻,步入了误区
强一致性存储系统的简单理解和定义
强一致性的理解:
resp返回给客户端之前,保证所有节点的内容都一致。
只要保证存储系统,在同步追加过程中强一致性,那么就能保证各个节点也为强一致性
至于是P2P系统还是主从系统,是如何的回滚机制,都不是强一致性考虑的内容。
存储系统的理解:
简单的分为KV存储系统和对象存储系统
1 最常见的存储系统key-value,代表有redis,mysql,mongodb等等
这些系统都是弱一致性模型的,对于并发写入,可以通过锁的机制(redis本身是单线程的,不存在这个问题),并发写在单线程的记录中,而后慢慢同步到从,就能保证主从的写入顺序是一致的
如果是强一致性要怎么做呢。例如对x进行多次写操作,必须得等同步机制完成,才能再写入,这个过程相当于存在写锁,而且这个锁耗时还较长。
如果是同时写x,y,那倒是没有问题。因此,强一致性的kv系统很少见。
2 对于对象存储系统来说,写入后再返回一个UUID,就能保证能高并发的写入
当然如果有对返回的UUID做追加写入操作,那还是存在阻塞写入的问题
我所做的系统,就是一个cache的对象存储系统,每次写入都是一次性的,写入后返回UUID。
显然不能每次写入都生成一个文件。
因此每次写入一个block,每个block在写入时是独占的,当所有空闲block的剩余容量都不够时。会新申请block进行写入。
由上层系统,将多个返回的UUID,offset,len在数据库中拼接成外部访问的同一个文件。
通过这种方式,来选择强一致性,不会影响吞吐量,仅仅影响响应时间。
基于主从系统的强一致性对象存储的理解
从主要是保证冗余系数以及可用性而存在的(主挂了以后从切换成主)
对于一个强一致性系统来说,如果一旦有节点挂掉,不管主从,都不再能写入,这样无疑是一致性最强的。
一 节点挂掉依然写入
如果有需求,需要在节点挂掉以后依然能够写入,如果没有机制约束,就会演变成弱一致性模型。
主要问题在于,新起的从如果没能达到一致,就切换成了主,同步会造成灾难性后果
观察一个具体的例子,针对一个单独的block的写入记录,括号内是已经提交的数据内容
1 | 主 commit_offset:5 data:abcde |
这里整个系统已经提交到5了,故障从还是3,然后主挂了,故障从成为主
1 | 故障旧主 commit_offset:5 data:abcfg |
故障从,新的提交fg覆盖了原来的提交
如上面的例子,故障从切换为主后,还未达成一致就切换成主,导致同步的内容覆盖了原有内容
解决方法是为故障重启的从添加一个recovery模式,该模式下的从不能成为主,直到和主最新的一条commit内容达成一致。
二 节点故障就不能写入的其他解决办法
从上述讨论可以看到,当节点故障还能写入,会使得解决办法的复杂度增加
我认为从工程的角度来看,最简单的方式是
强一致性系统,节点故障就进入只读模式
提供多组,每组主从的方式,随机选择每组进行写入,当写入失败,就换组再写,写完记录下组的ID
这种方式,从工程角度而言,简单的多
三 节点挂掉后不再写入的其他问题
问题是,不管是怎么样的提交回滚模式,在数据未达成一致前,有节点挂掉,有可能会导致脏数据
1 | 主 commit_offset:5 data:abcde |
主正在向第三个节点提交偏移量3,此时主挂了,整个系统的提交到3
1 | 从 commit_offset:5 data:abcde |
此时第三个节点其实才是正确的
对强一致性模型来说,数据最少的节点才是正确的
但是新起的主有脏数据de,使得两个从都被污染
这种脏数据的本质原因,还是因为新起的主,commit_offset大于real_commit_offset,这是无法避免的
下面具体看两种不同同步步骤的例子
强一致性主从模型的例子
交互较多的同步步骤
现在有个系统M,S1,S2,写入数据def
1 | 1 请求到主,req 数据内容为def->M,M根据commited位置写入数据 |
同步失败:
对主来说,任何一个向从发送请求失败,都应该立刻回复客户端操作失败,并且立刻将主设为只读
故障恢复:
1 从挂了又重启,直接将主设为可写,以commit为基准继续写入数据即可
4阶段以后的错误,都有可能造成脏数据
2 主挂了,重新选出的主,就有可能会遇到脏数据的问题
依然以commit为基准继续写入数据即可,这样脏数据相当于是无法被删除的数据
由于故障恢复的次数毕竟较少,因此从工程角度而言,是可以接受的
简化的同步步骤
现在有个系统M,S1,S2,写入数据def
1 | 1 请求到主,req 数据内容为def->M,M根据commited位置写入数据 |
同步失败:
对主来说,任何一个向从发送请求失败,都应该立刻回复客户端操作失败,并且立刻将主设为只读
故障恢复:
1 从挂了又重启,直接将主设为可写,以commit为基准继续写入数据即可
4-5之间出错,就可能造成脏数据
2 主挂了,重新选出的主,就有可能会遇到脏数据的问题
依然以commit为基准继续写入数据即可,这样脏数据相当于是无法被删除的数据
由于故障恢复的次数毕竟较少,因此从工程角度而言,是可以接受的
总结
1 从工程角度而言,强一致性模型,有节点挂掉,主就为只读,是简单有效的
能保证客户端认为写成功的数据不会丢失
2 强一致性模型,当出现故障以后,脏数据是无法简单避免的
客户端认为不会写成功的数据很有可能保存下来,而这从工程角度而言其实无伤大雅,对于一般需求的场景,没有必要做机制来处理