ddia, Distributed Data (Part 4): Troubles

这篇文章是ddia第八章的阅读笔记。

0x00 Intro

所有可能出错的地方一定会出错。

这篇文章来讲讲,分布式系统中会出现哪些错误。

知道会有什么错误,才能知道如何去解决。

0x01 Faults and Partial Failures

在单个节点上,系统要么功能正常,妖门完全失效,没有中间状态。

但是涉及到多个节点时,可能会出现部分正常部分失效的状态。

而且这个失效还是不确定的,不知道哪个节点在什么时候就可能发生一个错误导致失效了。

不确定性加部分失效,就很难。

0x02 Unreliable Networks

首先是网络不可靠。

在分布式系统的多个节点之间,没有共享内存,只能通过网络来传递信息。

发送方发送消息,接收方来接收,操作完成后给发送方返回结果,一来一往。

但是发送方发送之后,可能会出现很多问题:

  1. 请求发出去了,但是丢了;
  2. 请求在某个队列中排队呢,可能是网络或接收方已经超负荷;
  3. 接收方崩了;
  4. 接收方暂时无法响应(可能在GC);
  5. 接收方接收到了请求,也处理完了,但是响应暂时发不出去(超负荷);
  6. 接收方接收到了请求,也处理完了,但是响应丢了。

比如下图:

总之client没有收到响应,也不知道是网络问题还是server的问题。

一个简单的机制来处理这个问题:超时。如果超时了还没收到就认为收不到了。

2.1 超时与无期限的延迟

那么多长时间才算超时呢?

长了,意味着更长时间的等待;短了,意味着可能会出现误判。

假如一个虚拟的系统,数据传输延迟最大是d,请求在r内一定能处理完,那么最大的超时时间就是2d+r。

但是大多数系统并没有d和r的保证。

超时时间不是一个固定的值,而应该是一个变化的量,可以采用类似TCP的超时重试机制。

2.2 同步与异步网络

互联网的网络和电话网络的不同在于,电话网络是独占的,而互联网的网络是共享的。

这是为了提高资源的利用率。

但这样就导致不能像电话网络一样有一个可预测的延迟。

0x03 Unreliable Clocks

时间也很重要。

需要应用程序需要依赖时间,要么时间戳,要么时间段:

  1. 某个请求是否超时了?
  2. 某个服务99%的响应时间是多少?
  3. 在过去五分钟内,服务平均每秒处理多少个请求?
  4. 用户的停留时间是多少?
  5. 这篇文章什么时候发表的?
  6. 在什么时间发送提醒邮件?
  7. 这个缓存什么时候过期?
  8. 日志中的错误消息的时间戳是什么时候?

前四个涉及时间段,后四个涉及时间戳。

跨节点通信需要时间,所以收到消息的时间应该小于发送的时间。

但是由于不同节点的时间可能不同步,就会出现收到消息的时间小于发送时间。

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。

这就涉及到一个假设了,前面都是假设节点虽然不可靠但一定是诚实的。

不诚实的节点就更加复杂了,这就是拜占庭将军问题。

拜占庭将军问题就是,在不信任的环境中需要达成一致。

这个要求更高,这里就不涉及了。


 Previous
Functional Options Pattern in Go Functional Options Pattern in Go
Functional Options Pattern: 定义一个Options结构体(StuffClientOptions),包含所有的可选项; 定义一个函数类型,参数是Options结构指针(StuffClientOption); 创
2020-08-31
Next 
ddia, Distributed Data (Part 3): Transactions ddia, Distributed Data (Part 3): Transactions
这篇文章是ddia第七章的阅读笔记。 0x00 Intro 在一个系统中有很多可能出错的地方。 为了提高系统的可靠性,需要好好处理那些可能的错误。 事务(transaction)就是一个首选的机制。 这篇文章里主要看看事务。 0x01
2020-07-23
  You Will See...