所有可能出错的事情一定会出错
故障与部分失效
单节点的应用,质量合格情况下,操作有确定性。要么完全正常工作,要么就是完全失败(硬件问题,等)。
分布式系统,有多台节点的时,情况发生了根本变化。多接点复杂性,部分失效(网络分区)。
云计算和超算
- 一个极端是高性能计算。计算密集型的科学任务。
- 另一个计算是云计算。通用计算机用网络连接,弹性/按需分配资源。
- 传统企业数据中心介于二者之间。
基于互联网的系统,与高性能计算有很多不同:
- 服务是不可离线计算的,需要随时为用户提供低延迟服务。
- 硬件廉价的单节点聚合,故障率高。
- 节点之间通过网络连接
- 系统越大,局部组建失效的概率就越大
- HA的要求
- 节点之间额通讯可能不可靠。
所以系统的故障处理设计非常重要。软件要能够处理这种故障。要知道系统故障时候预期的行为。
不可靠的网络
基于不可靠的组建构建可靠的系统
- 纠错码
- TCP在不可靠的IP层提供可靠传输。
现实中的网络故障
人为+机器故障。发生概率高。
设计方面:出现网络问题,软件的应对策略需要容错和恢复。除了容错措施,还可以做的就是给用户提示错误信息。
运维方面:推荐人为的触发网络问题,来测试系统的反应情况。
检测故障
软件系统需要自动检测节点失效的功能。包括以下的设计:
- 不向已经失效的节点继续发送request
- 主从模式,主节点失效后,要触发选举新主。
确定问题出在哪里有可能很困难。如果想知道一个请求是否执行成功,需要应用级别的恢复。
超时与无限期的延迟
存在过场等待,误判节点死亡的可能。
2d+r设置理想的超时时间。但是异步网络系统对响应延迟没有任何保证。
网络延迟的根源是排队
- 多对一的发送,接收的单节点负载过重,发生拥塞。队列慢后被丢弃,引发重传。
- 接收放机器的CPU忙碌。处理延迟。
- 虚拟化导致的CPU占用。
- TCP流量控制
TCP vs UDP
不稳定网络环境下,超时设置:
- 超时的设置可以通过实验一步步确定。
- 自适应的动态超时调整。
同步与异步网络
拨打电话的网络是同步网络,预留了带宽资源。端到端的延迟有界。资源利用率低。
TCP是基于共享带宽的。IP协议本身就有排队的影响。有不确定性。但是资源利用率高。
延迟是成本和收益的相互博弈的结果。
不可靠的时钟
有两种时间问题:
- 测量持续时间。(request的发送到响应的时间间隔)
- 某个定点的时间值。(特定的日期时间,触发事件)
NTP协议,同步机器时钟。
单调时钟与墙上时钟
- 墙上时钟。某个时间点。Linux上的clock_gettime(CLOCK_REALTIME)和Java中的System.currentTimeMillis()
- 单调时钟,测量时间段。Linux上的clock_gettime(CLOCK_MONOTONIC),和Java中的System.nanoTime()都是单调时钟
时钟同步与准确性
需要同步的是单调时钟;硬件时钟和NTP可能会出现问题:
- 计算机中的石英钟不够精确:它会漂移(drifts)(运行速度快于或慢于预期)时钟漂移取决于机器的温度.
- 本地时钟和NTP时钟差距过大,出现时间突然倒退或者前进。
- NTP因为网络延迟,对精度产生影响。
- NTP服务器本身错误。
- 闰秒
- 虚拟化导致的一些毫秒级的延迟。
- 用户故意设置错误时间。
依赖同步的时钟
如果你使用需要同步时钟的软件,必须仔细监控所有机器之间的时钟偏移。时钟偏离其他时钟太远的节点应当被宣告死亡,并从集群中移除。这样的监控可以确保你在损失发生之前注意到破损的时钟
时间戳与事件顺序
跨节点的事件排序。使用墙上时钟。采用LWW策略会出现的问题。
节点之间的时间偏差会导致数据顺序排序有问题。
更安全的方式
所谓的逻辑时钟是基于递增计数器而不是振荡石英晶体,对于排序事件来说是更安全的选择(请参见“检测并发写入”)。逻辑时钟不测量一天中的时间或经过的秒数,而仅测量事件的相对顺序(无论一个事件发生在另一个事件之前还是之后)。相反,用来测量实际经过时间的时钟和单调钟也被称为物理时钟。我们将在“顺序保证”中查看更多订购信息。
时钟置信区间
使用公共互联网上的NTP服务器,最好的准确度可能达到几十毫秒,而且当网络拥塞时,误差可能会超过100毫秒
因此,将时钟读数视为一个精确的时间点是不应该的——它更像是一段时间范围:例如,一个系统可能以95%的置信度认为当前时间处于本分钟内的第10.3秒和10.5秒之间,它可能没法比这更精确了
Spanner中的Google TrueTime API,它明确地报告了本地时钟的置信区间。
全局快照的同步时钟
快照隔离中会需要单调递增的事务ID。如果一个写入发生在快照之后,那么这个数据就对该快照不可见。单机容易实现。
在多接点的环境中,需要复杂的协调产生这个唯一的递增ID。
(跨所有分区)全局单调递增的事务ID可能很难生成。事务ID必须反映因果关系:如果事务B读取由事务A写入的值,则B必须具有比A更大的事务ID,否则快照就无法保持一致。在有大量的小规模、高频率的事务情景下,在分布式系统中创建事务ID成为一个站不住脚的瓶颈。
更晚的事务会有更大的时间戳。当然,问题在于时钟精度的不确定性。Spanner以这种方式实现跨数据中心的快照隔离。 为了确保事务时间戳反映因果关系,在提交读写事务之前,Spanner在提交读写事务时,会故意等待置信区间长度的时间。这样读事务会处于足够晚的时间,两个事务之间的置信区间不会重叠。
进程暂停
例子:数据库主节点确定自己是否被其他节点宣布死亡,要用到租约。是一个带有超时的锁。任何一个时刻只有一个节点可以持有这个租约。
1 | while(true){ |
如果程序执行中出现了意外的停顿呢?例如,想象一下,线程在lease.isValid()行周围停止15秒,然后才终止。
发生的原因:
- GC
- 虚拟化挂起
- 操作系统上下文切换时,切换到另一个进程话费了更多的时间。
- 同步磁盘访问
- 内存页面抖动
所有的这些时间都是可以随时抢占正在运行的进程。在之后恢复。
分布式系统中的一节点不许假定执行过程中的任何时刻都可能被暂停,而且时间不确定。
响应时间保证
仔细调教系统,避免很多次的这种暂停。
调整垃圾回收影响
一个新兴的想法是将GC暂停视为一个节点的短暂计划中断,并让其他节点处理来自客户端的请求,同时一个节点正在收集其垃圾。如果运行时可以警告应用程序一个节点很快需要GC暂停,那么应用程序可以停止向该节点发送新的请求,等待它完成处理未完成的请求,然后在没有请求正在进行时执行GC。
这个想法的一个变种是只用垃圾收集器来处理短命对象(这些对象要快速收集),并定期在积累大量长寿对象(因此需要完整GC)之前重新启动进程
这些措施不能完全阻止垃圾回收暂停,但可以有效地减少它们对应用的影响。
知识,真相,谎言
真相由多数觉得(法定人数)
一个节点发生故障的可能性有很多。自身故障;网络通讯丢失;GC过长;
节点不能根据自己的信息来判断自身的状态。目前分布式算法都依靠法定票数。最常见的法定票数是取系统节点半数以上。
主节点与锁
只允许有一个实例存在的例子:
- 只允许一个节点作为数据库分区主节点
- 特定资源的锁(事务获取资源锁)
- 不能重复的用户名
要处理节点被宣布失效后的降级操作。
Fencing令牌
我们假设每次锁定服务器授予锁或租约时,它还会返回一个fencing令牌(fencing token),这个数字在每次授予锁定时都会增加(例如,由锁定服务增加)。然后,我们可以要求客户端每次向存储服务发送写入请求时,都必须包含当前的fencing令牌。
对于不明确支持fencing令牌的资源,可能仍然可以解决此限制(例如,在文件存储服务的情况下,可以将防护令牌包含在文件名中)。但是,为了避免在锁的保护之外处理请求,需要进行某种检查。
拜占庭故障
伪造令牌,有节点故意破坏。
拜占庭故障(Byzantine fault),在不信任的环境中达成共识的问题被称为拜占庭将军问题
现实中的例子:
- 航空航天的硬件,被辐射发生故障。
- 在多个参与组织的系统中,一些参与者可能会试图作弊或者欺骗他人。
解决拜占庭容错的系统协议异常复杂;容错的嵌入式系统还需要硬件层面的支持。
普通的web应用不需要拜占庭容错,因为可以由服务其全权觉得请求是不是合法。在那种没有这种中央决策机制的点对点网络中,拜占庭容错才更为必要。
大多数拜占庭式容错算法要求超过三分之二的节点能够正常工作。
如果攻击者可以渗透一个节点,那他们可能会渗透所有这些节点,因为它们可能运行相同的软件。因此传统机制(认证,访问控制,加密,防火墙等)仍然是攻击者的主要保护措施。
弱的谎言形式以及预防:
当系统由于硬件,bug,配置错误发生了错误,可以采取一些方案来让系统更可靠:
- TCP/UDP内置的校验和
- 总是检查用户的输入是否合法
- NTP客户端配置多个时间服务器
理论系统模型与实践
计时方面:
- 同步模型
- 部分同步模型
- 异步模型
处理节点失效:
- 崩溃-中止模型
- 崩溃-恢复模型
- 拜占庭(任意)失效模型
对于fencing令牌生成算法模型要求:
- 唯一性
- 单调递增性
- 可以用性
安全性和活性
安全性通常被非正式地定义为,没有坏事发生,而活性通常就类似:最终好事发生。但是,最好不要过多地阅读那些非正式的定义,因为好与坏的含义是主观的。
在刚刚给出的例子中,唯一性(uniqueness)和单调序列(monotonic sequence)是安全属性,但可用性是活性(liveness属性。