这篇文章是ddia第八章的阅读笔记。
0x00 Intro
所有可能出错的地方一定会出错。
这篇文章来讲讲,分布式系统中会出现哪些错误。
知道会有什么错误,才能知道如何去解决。
0x01 Faults and Partial Failures
在单个节点上,系统要么功能正常,妖门完全失效,没有中间状态。
但是涉及到多个节点时,可能会出现部分正常部分失效的状态。
而且这个失效还是不确定的,不知道哪个节点在什么时候就可能发生一个错误导致失效了。
不确定性加部分失效,就很难。
0x02 Unreliable Networks
首先是网络不可靠。
在分布式系统的多个节点之间,没有共享内存,只能通过网络来传递信息。
发送方发送消息,接收方来接收,操作完成后给发送方返回结果,一来一往。
但是发送方发送之后,可能会出现很多问题:
- 请求发出去了,但是丢了;
- 请求在某个队列中排队呢,可能是网络或接收方已经超负荷;
- 接收方崩了;
- 接收方暂时无法响应(可能在GC);
- 接收方接收到了请求,也处理完了,但是响应暂时发不出去(超负荷);
- 接收方接收到了请求,也处理完了,但是响应丢了。
比如下图:
总之client没有收到响应,也不知道是网络问题还是server的问题。
一个简单的机制来处理这个问题:超时。如果超时了还没收到就认为收不到了。
2.1 超时与无期限的延迟
那么多长时间才算超时呢?
长了,意味着更长时间的等待;短了,意味着可能会出现误判。
假如一个虚拟的系统,数据传输延迟最大是d,请求在r内一定能处理完,那么最大的超时时间就是2d+r。
但是大多数系统并没有d和r的保证。
超时时间不是一个固定的值,而应该是一个变化的量,可以采用类似TCP的超时重试机制。
2.2 同步与异步网络
互联网的网络和电话网络的不同在于,电话网络是独占的,而互联网的网络是共享的。
这是为了提高资源的利用率。
但这样就导致不能像电话网络一样有一个可预测的延迟。
0x03 Unreliable Clocks
时间也很重要。
需要应用程序需要依赖时间,要么时间戳,要么时间段:
- 某个请求是否超时了?
- 某个服务99%的响应时间是多少?
- 在过去五分钟内,服务平均每秒处理多少个请求?
- 用户的停留时间是多少?
- 这篇文章什么时候发表的?
- 在什么时间发送提醒邮件?
- 这个缓存什么时候过期?
- 日志中的错误消息的时间戳是什么时候?
前四个涉及时间段,后四个涉及时间戳。
跨节点通信需要时间,所以收到消息的时间应该小于发送的时间。
但是由于不同节点的时间可能不同步,就会出现收到消息的时间小于发送时间。
3.1 单调时钟和钟表时钟
现代计算机内部有两种时钟:一个是钟表上的时钟(time-of-day clock),一个是单调时钟(monotonic clock)。
钟表上的时钟返回当前的日期和时间,比如Linux的clock_gettime(CLOCK_REALTIME)
,这是一个时间点。
这个时钟可以和时间服务器NTP同步,这时钟表时钟就会出现回拨或者向前跳跃的现象。
单调时钟更适合用来测量时间段,比如服务响应时间,Linux中的clock_gettime(CLOCK_MONOTONIC)
。
单调时钟保证总是向前,不会出现钟表时钟的回拨或跳跃现象。
3.2 依赖同步的时钟
如果应用需要精确同步的时钟,最好仔细监控所有节点上的时钟偏差。
3.2.1 时间戳与事件顺序
跨节点的事件,就依赖同步的时钟。如果时钟不同步,就会出现问题。比如:
Client B 的写入时间明明比A晚,但是B的时钟却早于A,导致错误。
3.2.2 全局快照的同步时钟
常见的快照隔离实现中需要单调递增的事务ID。
但是当需要多个节点时,需要有一个服务来产生全局的、单调递增的事务ID。
3.3 进程暂停
另一个危险的例子就是进程暂停。
加入一个系统中只有一个主节点,其他节点需要知道这个主节点有没有失效。
一种方式是通过一个服务获取lease,类似一个带有超时的锁。
某个节点拿到这个lease后就可以被认为是主节点,在过期之前续期,成功了就继续是主节点。
大概这样:
while(true) {
request = getIncomingRequest();
// Ensure that the lease always has at least 10 seconds remaining
if(lease.expiryTimeMillis - System.currentTimeMillis() < 10000) {
lease = lease.renew();
}
if(lease.isValid()) {
process(request);
}
}
这是有问题的,比如第一个if通过之后,系统GC了,停了15秒,这个时候这个lease已经过期了,就可能有其他的节点变成了新的主节点,但是这个节点还认为自己是主节点,boom。
除了GC,还有很多情况会导致进程中途停止一段时间:
- 虚拟化环境中,可能会暂停虚拟机然后继续;
- 操作系统上下文切换;
- SIGSTOP信号。
等等。
0x04 Knowledge, Truth, and Lies
4.1 真相由多数决定
多个节点中,如果一个节点本来是正常的,只是由于网络原因不能与其它节点通信,那么其余的节点就会认为这个节点出现了故障。
也就是说,节点不能根据自己的信息来判断自身的状态。
最常见的法定票数是取系统节点半数以上,quorum。
对于前面lease的问题,可以通过fencing技术来解决。
简单来说就是获取lease的时候返回一个fencing令牌,这个令牌每授予一次就增1,每次发送写请求的时候带上这个令牌,通过这个令牌可以检测过期的lease。
如图:
也就是说,只靠客户端自己检查锁的状态是不够的,需要资源本身主动检查,不过这会使服务器实现变得复杂。
4.2 拜占庭故障
fencing技术有一个问题,就是客户端可以伪造一个更大的fencing。
这就涉及到一个假设了,前面都是假设节点虽然不可靠但一定是诚实的。
不诚实的节点就更加复杂了,这就是拜占庭将军问题。
拜占庭将军问题就是,在不信任的环境中需要达成一致。
这个要求更高,这里就不涉及了。