levy5307 / blog

https://levy5307.github.io/blog/
MIT License
0 stars 0 forks source link

Chubby #28

Open levy5307 opened 4 years ago

levy5307 commented 4 years ago

https://levy5307.github.io/blog/Chubby/

一致性client库 or 分布式锁

chubby实现的是一个中心化锁所服务,基于其一些优势:

服务改造的时候,直接用分布式锁更容易实现 许多服务有小数据存储的要求,chubby提供了小数据存储 对大部分程序员更熟悉锁 一致性client库需要client拥有多台机器来做投票选举

粗粒度锁 or 细粒度锁

细粒度锁指的是只会持有锁很短的时间(秒级或更短),而粗粒度锁指的是持有比较长的时间,比如说是用于选主,通常都会是几小时甚至几天。这两种粒度的锁对锁服务提出了不同的要求。

粗粒度的锁因为隔很长的时间才需要访问锁服务一次,所以对server端的负载压力很小,并且这个负载跟client端的处理速率关联很小(意思是即使client端每秒处理很多请求,锁服务的server端收到的请求速率也不会明显增加)。另外,锁服务server端的机器故障对client的影响也比较小。

细粒度的锁就完全相反了,server端的失败可能造成很多client阻塞。性能和扩容的能力都很重要,因为server端的负载和client的处理速率密切相关。 Chubby只试图提供粗粒度的锁,客户端也可以直接根据他们的应用实现细粒度锁。

系统结构

Chubby主要有两个组成部分,server和client library。Chubby的一个个cell中有五台server组成一个集群,其中一个master,其他为slave。五台机器按照Paxos协议选举出master,要想成为master必须得到五台中大多数的投票。master会以租约的形式运行,只要它能得到副本的同意(即续租),那么租约便能定时更新然后延长master的运行时间。

客户端会向DNS请求Chubby服务器列表,然后随机发起访问,非master服务器会依据自身的存储的集群信息向客户端反馈哪台是master,然后客户端重定向到master。

如果一个副本挂了,并且几个小时都没有恢复,一个替换系统就会选一台新机器,启动chubby服务,然后更新DNS table。master会定期从DNS table拉数据,就会得知这个变化,然后在数据库中更新cell成员的列表,这个列表也是通过普通的一致性协议在副本间维护一致性的。

与此同时,新的副本会从存储在文件系统的数据库备份中取得最近几次的拷贝,并且在活跃的的那些副本那里取得更新。等这个机器成功处理了master的一个等待commit的请求后(说明此时数据已经是最新了),这个机器就可以参与投票了。

文件、目录

chubby的文件与unix文件系统基本一样,这样的好处是chubby文件既可以被chubby api访问,也可以被其他文件系统(例如GFS)的api访问。

/ls/foo/wombat/pouch

ls即lock service,是所有Chubby的文件名字通用的前缀,并且代表了锁服务。第二个组件(foo)是Chubby cell的名字

有临时和永久的节点,所有节点都可以被显示的删除,临时节点当没有client打开它们时就会被自动删除,所以临时的节点可以被用来检测client是否存活。

每个节点都有各种元数据,包括三个ACLs名字,这三个ACLs用于控制读取、写入和更改ACL名字。ACLs其实是位于ACL路径之下的文件。如果文件F写了其ACL名称为foo,ACL目录包含了文件foo,该文件中有一个叫做bar的条目,那么接下来用户bar将有权限对文件F做写入。

强制锁 or 建议锁

强制锁指的是当client没有持有锁时则不可访问资源

建议锁指的是只有其它client想要持有同样的锁时才会产生冲突,持有锁对于访问资源来说并不是必要的。

Chubby采用的是建议锁,理由如下:

Chubby锁常常保护其它服务的资源,而不是Chubby中跟锁关联的文件,而使用强制锁往往意味着要对其它服务做额外的修改。

当用户需要访问锁住的文件进行调试或管理目的时,我们并不想用户关掉程序。

我们的开发者使用很常见的错误检测方式,写assert语句比如‘assert 锁X被持有了’,所以强制锁的方式对他们来说意义不大。

Sequence

在分布式系统中锁是复杂的,因为消息是不确定的,进程也可能会挂掉。

举个例子,一个进程持有一个锁L,然后发起一个请求R,然后挂掉。另一个进程就会去持有这个锁L,然后在R到达前做一些操作。等R到达后,它可能就会在没有锁L的保护下进行操作,潜在的会造成不一致的数据。

这里的意思是这个锁L保护了一段数据data,按理说这个R应该在这个data上进行操作的,但是由于进程挂掉,导致另一个进程修改了这个data,所以R就可能在不一致的数据上进行操作。

Chubby提供了一种在使用锁的时候使用序列号的方法来解决这个问题。在每次获得锁后都会请求一个序列号(一个描述锁状态的字符串),client在发送请求的时候,会把这个序列号发给服务端,服务端会检测这个序列号的合法性。服务端可以通过和Chubby之间维护的cache来检测这个序列号的合法性,或者是直接和自己最近观测到的序列号比较。服务端可以通过和Chubby之间维护的cache来检测这个序列号的合法性,或者是直接和自己最近观测到的序列号比较

尽管序列号机制很简单,但是有些协议发展的很慢,不能带上序列号,chubby因此提供了另一种不完美但是更容易的方式来解决这个问题。如果一个client是以正常的方式释放锁的,那么这个锁立刻可以被其他的client获得,但是如果一个锁是因为client挂掉或不可访问而丢掉的,锁服务器会等一段叫lock-delay的时间来防止其它的client获得这个锁

Events

Chubby的client端可以订阅一些事件,这些事件通过回调的方式异步发送给client,包括:

文件内容修改

子结点的添加、移除、或修改。

Chubby主节点出故障

一个句柄(包括它的锁)已经变得不可用

获得锁

来自其他客户端的锁冲突请求

Cache

客户端会有自己的本地缓存,这样可以减少对Chubby的读压力。Chubby使用的是一致性的、write-through缓存。该缓存由记录有缓存的客户端列表的master发送出来的无效信号保持一致性。协议确保客户端能够互相看到一个一致的Chubby状态视图,或者是一个错误。

当文件的数据或者元数据被改变时,修改会被阻塞,直到master发送数据的无效信号到每一个可能缓存该数据的客户端上。修改只会有在服务器知道每个客户端都将缓存置为无效后才会被处理,要么是因为客户端确认了缓存的失效,要么是客户端确认其缓存的数据租约到期。

Session and KeepAlives

Chubby的会话是指一个Chubby客户端与一个Chubby单元之间的联系;它会存在一段时间,并且通过周期性的握手(即KeepAliveb)来维护。当会话有效时,client端的handle,锁,缓存都是有效的。

当client第一次连接cell时,它会请求一个新的会话,当client结束时会显示的终止会话,或者当这个会话一分钟内没有调用和打开handle时,也会被隐式的关闭

每个会话都有个对应的租约,master承诺在租约内不会单向的关闭会话,master可以延长这个租约,但不能减少。收到KeepAlive后,master会阻塞这个RPC,直到client的租约接近过期,然后master会允许这个RPC返回,就可以通知client新的租约超时时间。master可以任意扩展租约超时,默认是12s,但是过载的master可以指定更大的值来减少KeepAlive RPC的数量。client在收到响应后,就会马上发起一个新的KeepAlive,因此几乎总是有一个KeepAlive被阻塞在master。 除开用来扩展租约之外,KeepAlive还被用来传递事件和缓存失效给client。如果事件或者缓存失效发生了,master允许KeepAlive立刻返回。

同时客户端维护了一个本地租约过期时间,如果客户端的本地缓存租约过期了,由于此时它就无法确定 master 是否已经结束了这个 session,客户端就需要清空并禁用它的缓存,此时 session 处于 jeopardy 状态。客户端会继续等待一个称为 grace period 的时长,默认是45秒。如果在grace period 结束之前,客户端和 master 又完成了一次成功的 KeepAlive 交互,那么客户端就会再次使它的缓存有效。否则,客户端就假设 session 已过期。

Chubby的client库可以通知应用程序jeopardy事件,当会话恢复正常时,会通知应用程序safe事件,当会话超时时,会通知应用程序超时事件。这些信息使应用程序可以知道会话的状态,在不确定会话是否关闭时可以停下来等一会儿,如果只是个临时性的问题的话,就可以自动恢复而不用重启应用。这避免了应用重启的巨大开销。

Fail-overs

在master挂掉的时候,如果master选举很快,那client可以在自己的本地超时过期前就联系上新的master;否则,client的本地超时过期后,client可以利用grace period来让会话在fail-over期间得以维持,也就是说,grace period其实增加了client端的租约超时时间。

上图是client端在master fail-over时利用grace period来保留会话的一个例子。从图中可以看到client的本地租约已经超时,client进入了jeopardy状态,在grace period期间,client成功的联系上了新的master。一旦client成功联系上新master,对应用程序而言,就像是没有失败发生一样。为了实现这个,新master必须要重建旧master的内存状态,一个新选举出来的master需要进行的流程:

首先选一个新的client epoch号,client需要在每个调用中带上这个epoch号。master会拒绝使用旧epoch号的client端,这可以防止新master对一个很老的发送给旧master的包作出响应。

新master可能会对master-location请求作出响应,但不会对跟session有关的请求作出响应。

新master根据数据库中持久化的信息在内存中构建锁和会话的数据结构,会话的租约被扩展到之前的master可能已经使用的最大值。

master现在允许client执行KeepAlive

给每个会话生成一个fail-over事件,使client端刷新缓存,因为client可能错过了缓存失效事件,并且警告应用程序其它事件可能也丢掉了。(因为旧master挂的时候可能还来不及发送各种事件就挂了)。

master等待client端确认fail-over事件或者是client端的会话超时。

master允许所有的操作正常进行。

如果client使用一个旧的handle,新的master会在内存中构建这个handle的状态。如果这个重建的handle后续被关了,master也会在内存中记录下来,使得在这个master的任期内不可能再重新创建一个相同的handle。

在一段时间后,master会把没有handle打开的临时文件给删了,因此client端需要在这段时间内刷新自己对临时文件的handle。这个机制有个不好的地方是在fail-over期间如果一个临时文件的所有client端都失去了会话,这个临时文件也不能及时被删除(需要等这段时间结束,通常是1min)