异常处理
Dynamo中把异常分为两种类型,临时性的异常和永久性异常。服务器程序运行时一般通过类似supervise的监控daemon启动,出现core dump等异常情况时自动重启。这种异常是临时性的,其它异常如硬盘报修或机器报废等由于其持续时间太长,称之为永久性的。回顾Dynamo的设计,一份数据被写到N, N+1, ... N+K-1这K台机器上,如果机器N+i (0 <= i <= K-1)宕机,原本写入该机器的数据转移到机器N+K,机器N+K定时ping机器N+i,如果在指定的时间T内N+i重新提供服务,机器N+K将启动传输任务将暂存的数据发送给机器N+i;如果超过了时间T机器N+i还是处于宕机状态,这种异常被认为是永久性的,这时需要借助Merkle Tree机制进行数据同步。这里的问题在于时间T的选择,所以Dynamo的开发人员后来干脆把所有程序检测出来的异常认为是临时性的,并提供给管理员一个utility工具,用来显示指定一台机器永久性下线。由于数据被存储了K份,一台机器下线将导致后续的K台机器出现数据不一致的情况。这是因为原本属于机器N的数据由于机器下线可能被临时写入机器N+1, ... N+K。如果机器N出现永久性异常,后续的K台机器都需要服务它的部分数据,这时它们都需要选择冗余机器中较为空闲的一台进行同步。Merkle Tree同步的原理很简单,每个非叶子节点对应多个文件,为其所有子节点值组合以后的Hash值,叶子节点对应单个数据文件,为文件内容的Hash值。这样,任何一个数据文件不匹配都将导致从该文件对应的叶子节点到根节点的所有节点值不同。每台机器维护K棵Merkle Tree,机器同步时首先传输Merkle Tree信息,并且只需要同步从根到叶子的所有节点值均不相同的文件。
读/写流程
客户端的读/写请求首先传输到缓存的一台机器,根据预先配置的K、W和R值,对于写请求,根据DHT算法计算出数据所属的节点后直接写入后续的K个节点,等到W个节点返回成功时返回客户端,如果写请求失败将加入retry_list不断重试。如果某台机器发生了临时性异常,将数据写入后续的备用机器并在备用机器中记录临时异常的机器信息。对于读请求,根据DHT算法计算出数据所属节点后根据负载策略选择R个节点,从中读取R份数据,如果数据一致,直接返回客户端;如果数据不一致,采用vector clock的方法解决冲突。Dynamo系统默认的策略是选择最新的数据,当然用户也可以自定义冲突处理方法。每个写入系统的<key, value>对都记录一个vector lock信息,vector lock就是一系列<机器节点号, 版本号/时间戳>对,记录每台机器对该数据的最新更新版本信息。如下图:
读取时进行冲突解决,如果一台机器读到的数据的vector lock记录的所有版本信息都小于另一台机器,直接返回vector lock较大的数据;如果二者是平行版本,根据时间戳选择最新的数据或者通过用户自定义策略解决冲突。读请求除了返回数据<key, value>值以外还返回vector lock信息,后续的写操作需要带上该信息。
问题1:垃圾数据如何回收?
Dynamo的垃圾回收机制主要依赖每个节点上的存储引擎,如Berkely db存储引擎,merge-dump存储引擎等。其它操作,如Merkle Tree同步产生的垃圾文件回收可以和底层存储引擎配合完成。
问题2:Dynamo有没有可能丢数据?
关键在于K, W, R的设置。假设一个读敏感应用设置K=3, W=3, R=1,待处理的数据原本属于节点A, B, C,节点B出现临时性故障的过程中由节点D代替。在节点B出现故障到节点B同步完成节点D暂存的修改这段时间内,如果读请求落入节点B或者D都将出现丢数据的问题。这里需要适当处理下,对于B节点下线的情况,由于其它机器要么缓存了B节点已下线信息,要么读取时将发现B节点处于下线状态,这是只需要将请求转发其它节点即可;对于B节点上线情况,可以等到B节点完全同步以后才开始提供读服务。对于设置W<K的应用,Dynamo读取时需要解决冲突,可能丢数据。总之,Dynamo中可以保证读取的机器都是有效的(处于正常服务状态),但W != K时不保证所有的有效机器均同步了所有更新操作。
问题3:Dynamo的写入数据有没有顺序问题?
假设要写入两条数据"add item"和"delete item",如果写入的顺序不同,将导致完全不同的结果。如果设置W=K,对于同一个客户端,由于写入所有的机器以后才返回,可以保证顺序;而多个客户端的写操作可能被不同的节点处理,不能保证顺序性。如果设置W < K,Dynamo不保证顺序性。
问题4:冲突解决后是否需要将结果值更新存储节点?
读操作解决冲突后不需要将结果值更新存储节点。产生冲突的情况一般有机器下线或者多个客户端导致的顺序问题。机器下线时retry_list中的操作将丢失,某些节点不能获取所有的更新操作。对于机器暂时性或者永久性的异常,Dynamo中内部都有同步机制进行处理,但是对于retry_list中的操作丢失或者多个客户端引发的顺序问题,Dynamo内部根本无法分辨数据是否正确。唯一的冲突解决机器在读操作,Dynamo可以设计成读操作将冲突解决结果值更新存储节点,但是这样会使读操作变得复杂和不高效。所以,比较好的做法是每个写操作都带上读操作返回的多个版本数据,写操作将冲突处理的结果更新存储节点。