分布式系统概念简介及其问题的描述

目录

  1. 什么是分布式系统
  2. 分布式系统能够解决什么问题
    2.1 服务器的性能扩展方案
    2.2 分布式系统的特点
    2.3 分布式系统解决的问题
  3. 分布式系统的问题
    3.1 网络问题
    3.2 时钟问题
    3.3 节点问题
  4. 在不可靠硬件构建可靠软件需要满足的条件和假设
    4.1 真相由多数决定
    4.2 理论系统模型与现实
  5. ShareNote

#什么是分布式系统

分布式系统

1
2
3
// Definantion from IBM Site 
A distributed computer system consists of multiple software components that are on multiple computers, but run as a single system. The computers that are in a distributed
system can be physically close together and connected by a local network, or they can be geographically distant and connected by a wide area network. A distributed system can consist of any number of possible configurations, such as mainframes, personal computers, workstations, minicomputers, and so on. The goal of distributed computing is to make such a network work as a single computer.

简述:
分布式的计算机系统是由多台形式的计算机(包括服务器、个人电脑、工作站、嵌入式系统的形式)组成的,但是对外表示成一个完整功能的系统。对于分布式系统的使用者,可以把一个大型的分布式系统看作一个黑盒来进行使用。

#分布式系统能够解决什么问题

2.1. 服务器的性能扩展方案

对于一个系统来说,如果当需要计算的问题太过庞大的时候,会有两种思路来进行算力的添加

1.Scale UP(纵向扩展)
纵向扩展:企业后端大型服务器以增加处理器等运算资源进行升级以获得对应用性能的要求。
这种是比较传统的思路,具有以下特点:
1. 扩容的成本较高。能够支持纵向扩展的系统都需要服务商进行服务的支持,需要购买特殊的硬件。而不是使用能够通用的硬件来进行扩容
2. 延迟低,单点吞吐量大。本质上是单点的系统,可以减少了很多网络上面的开销,所以延迟一般会比分布式系统的延迟更低。而且单节点如果在没有超过负荷的情况下,因为硬件堆积在上面,单节点的性能一般会比分布式的单节点性能更强。
3. 备份成本高,遇到不可恢复的故障时可能要中断一定时间的服务。如果对于纵向扩展的设备需要进行灾备的情况下,可能需要同时购入相同配置的硬件通过配置主备来继续容灾,而不能通过像分布式系统在多个地域继续节点部署和配置添加达到冗余的程度。
实例的系统: 传统的SAN(storage area network)系统

2.Scale Out(水平扩展)
水平扩展:企业可以根据需求增加不同的服务器应用,依靠多部服务器协同运算,借负载平衡及容错等功能来提高运算能力及可靠度。
这是从谷歌的三篇经典的分布式论文(bigtable、distributed filesystem、 mapreduce)提炼出来的特点:
1. 成本低,扩展性好。使用的是市面上可以购买的通用的服务器配件,并且可以根据需求进行硬件的升级即可提升整体的性能(如存储服务器上面,把HDD更换为全闪存的NVME SSD,立刻可以在读写的吞吐量和时延上面获得具体的提升)
2. 弹性好。可以通过配置动态的增删服务器,可以动态的对业务使用的服务器资源具有更好的调配,并且可以通过弹性的配置获得根据地理位置的容灾以及数据冗余功能(如金融企业中的两地三中心的需求)
3. 处理单节点无法处理的问题。(最重要)

2.2 分布式系统的特点

  1. 扩展性强(Scalability)
  2. 有冗余(Redundancy)

2.3 分布式系统解决的问题

分布式系统最重要解决的问题是:

  1. 处理单机无法计算的问题。
    因为问题能够被分治成为了更小的问题,使得更弱的节点也可以进行计算
  2. 同样的工作量下,减少计算使用的时间。
    因为部分计算是可以并行来继续执行的,只要调度得当的情况下,可以减少单机系统中出现的等待的情况。
  3. 在有限的成本内,可以通过多区域部署服务来降低该地区的服务的时延。

分布式系统的问题

(此部分有部分会引用到数据密集型应用系统设计(DDIA)的第8章部分的内容)
软件工程的世界里面没有银弹,分布式系统并不是所有问题的最优解。它在解决上述问题的情况下也引入了下面的多种问题
此处需要引入一个部分失效的概念:

1
部分失效:在分布式系统中,可能会出现系统的一部分工作正常,其他部分出现难以预测的故障。

对于互联网服务的分布式系统的假设

  1. 需要7*24的可用状况,所以不能有服务不可用的状态
  2. 采用通用的硬件,故障率会较高,成本较低
  3. 采用IP和以太网的技术,网络可靠性不如专有网络(Fiber Channel等。。。)

因为上面的三个原因可能引申出来的另外的假设

  1. 系统越大,局部组件失效的概率越大。(长时间的运行时间,失效、修复、再失效是正常的状况而不能当为异常的情况去考虑)
  2. 由于局部失效的概率高,因此需要必须容忍部分节点失败,并且能够保持对外提供可用的服务

所以实际上我们目前大部分的分布式的系统是需要在不可靠的硬件上面通过软件容错来构建可靠的系统。

所以分布式系统遇到的问题可以分为三个大类

  1. 网络类问题
  2. 时钟问题
  3. 节点软件和硬件的问题

网络问题

对于上面的假设(主要是成本考虑)来说,一般都会采用无共享的方式来构建集群。

由于通常使用的都是以太网来构建,而且以太网是异步的网络。可能会出现下面的这些情况

  1. 请求丢失
  2. 请求再队列等待,无法发送(网络问题或者接收方繁忙在内核的队列中等待)
  3. 远程接收节点失效(节点崩溃)
  4. 远程节点无法响应
  5. 远程节点有返回,但是返回过程中丢失
  6. 远程节点处理请求,但是在返回时候被延时处理(优先级问题, 或者网络问题或则和发送方阻塞了)

此处需要多引入一个新概念,网络分区

1
网络分区(Network Partition):当网络的一部分由于网络故障而与其他部分隔开,称之为网络分区或者网络分片

在工程的实践中,网络故障(丢包、链路中断、路由问题、物理设备损坏)的概率是远比想象中高, 因此在基于我们假设的前提的环境下面,必须要把网络分区和网络故障接入到软件中进行容错。

对于如何处理检测是否故障,需要判断的一个问题是,是节点服务上面出现了真实的故障,还是网络的不可靠导致的服务失效的表现。

1
2
3
4
5
6
在实际生产的实践中有这样的方法来快速的在本应用层(网络正常的情况下)来进行简单的判断
1. 通过服务注册的心跳来注册自己的服务存活,并且把死亡状态的服务实例剔除出可用服务中
2. 检测进程是否崩溃,如果崩溃发送一个信号。(或者在我的工作经历中,使用一个进程去定时检测进程是否存活,失败了发送信号使得资源进行重启的尝试,并且上报告警)
3. 通过交换机去查询网络的状况
4. 通过中间件的自身监控(如Rabbitmq的web界面)来监控中间件的执行状况
5. 定期去尝试访问网络是否通达

但是最终我们唯一能够完全判断故障的方法只有超时,来保证节点是不可用的状态(为了保证网络的不可靠问题)。

超时引入的问题

我们如何能够判断什么时候才是最好的超时的判断间隔呢?

对于上面的这个问题, 我们引入两个问题的前置的讨论

  1. 是什么导致我们会出现网络的不稳定
  2. 如果出现超时之后,会有怎样的影响,会导致什么样的后果

网络不稳定的原因

根本的原因: 在设计目前的TCP/IP的协议和方法的时候,我们的目的是为了尽可能高的提高资源的利用率
设计的思想: 通过动态分配和竞争的机制使得动态决定那个包被发送,但是这样的方法必然会引入排队的机制

1
2
3
4
// 出现网络不稳定的原因可能如下
1. 对于交换机的角度,会有多条等待队列来进行数据报文的转发,如果符合过重,则需要等待轮询机制来等待发送的机会,并且如果数据量过多的情况下,也有可能出现数据报文被丢弃,然后被不断重试的现象
2. 在服务器的处理中,因为Linux内核处理接收到的报文也是类似于交换机的处理模式,通过队列来对接受到的包进行排序以及复制到用户态,因此也是需要一定的时间需要处理
3. 在TCP的协议中的重传的机制也会根据返回的报文的控制字段来减小发送窗口的大小来减少对接受方的压力,可能导致排队会提前到发送方的等待队列中。并且重传机制会进行多次的重试,引入了隐形的超时时间

调用超时(网络超时)可能会导致的后果

主要的影响是发生了责任的转移,可能带来可能的影响

  1. 命令的重复执行
  2. 数据迁移
  3. 当整个系统处于高负荷的状态,可能会把其他正常的部分通过转移职责导致整个系统被拖垮

因此我们必须很小心调整这个超时的阈值,如果在以前的情况下,可能需要系统管理员对于在生产环境的情况下调整一定的参数来慢慢测试得出一个最优值。
但是在最近的组建的实现中有两种新的思路

1
2
1. 通过内部的一个故障检测器来计算成功率来进行对超时阈值的控制(Akka, Cassandra, Hytrsix中有使用此种方法)
2. 通过对数据的采集清洗和分析(AIOPS)(可能是APM(Application Performance Metric)的埋点采集数据以及对监控数据的整体清洗通过一些机器学习的模型来计算出超时的阈值)

时钟问题

分布式的系统的时钟问题也是一个非常值得重视的问题,后面会有我自己的工作案例来说明可能出现的情况的严重性。

其实对于时间我们一般分为两种的应用方式

  1. 在一段时间内的统计数据(时间段)
  2. 某些时间点是否有指定动作完成或某个历史行为的发生的时间点

服务器时间的维护方式以及问题

每个机器的时间可以通过两种方式来共同维护

  1. 本地时间(通过主板的石英振荡器来维护)
  2. 通过与一个准确的时间获取源来定是同步时间(NTP和Chrony就是服务于方面的协议)

这两种的结合不一定能够保证这样的可信赖的分布式集群中的时间就必然是同步的

  1. 本地时间本来每个节点都可能会出现偏差
  2. 同步时间的时候必须通过网络来进行同步,而在我们上面提及的假设中,网络是不可靠的,因此可能会出现失败的情况。
  3. NTP如果发现时间差别太大(实际生产中是时间差>=10分钟的情况下)的话会拒绝同步(但是Chrony并没有此问题)
  4. 闰秒的问题

当然可以像Google维护Spanner集群那样采用高精度的原子钟加上GPS定位来保证错误在一定的可接受范围内运行(一般误差的单位为5微妙)。但是不是每个分布式系统都能够有如此之高的维护成本去使用这种方式来

时钟的概念

时钟可以分解为两个时钟的概念

  1. 物理时钟
    物理时钟:包括了两种一个是墙上时钟,一个是单调时钟。可以理解为可以通过系统时间相关的API进行获取的时钟。
  2. 逻辑时钟
    逻辑时钟:可以理解为分布式系统中全局的单调递增的事件ID,事件发生顺序与此相同。

物理时钟

需要先引入一个时间调整的概念:

如果把时间以时间轴的方式来进行表达,往时间变大的方向调整为向前调整,往时间变小的方向调整的是向后调整。
只要发生了时间的变动,我们都可以称之为服务器发生了时间跳变。

1. 墙上时钟

根据日历返回当前的时钟,就是我们平时在Linux上直接使用date命令或者语言内置的时间相关的api的输出。
这种时钟可以与时间同步服务器后同步后进行修改,可能会出现向前调整或者向后调整的可能性

2. 单调时钟

用于: 适合测量持续的时间段
单调时钟的特性:
1. 保证只会向前调整
2. 不需要进行同步

计算和调整的方式

在一个时间点进行对单调时间的获取,然后完成某项工作后,再去检查时间。但是这段时间的差值并没有太大的意义。

时间与事件的顺序问题

当两个客户端对于一个分布式的数据库(非Raft或者Paxos类型,如Riak)分别进行写入的情况下(此处假设先写入的客户端为A,晚写入的客户端为B)

当B虽然在发生的时间比A要晚,但是B在集群中同步完成的时间比A要早的情况写。即对外部来说A还未写入成功的情况下,先执行了B的操作,可能导致B的操作丢失(如果B的操作是依赖于A先写入的操作)。
上面是一个非常经典的关于分布式集群时间与事件的顺序问题。然而这种问题即使在时间同步服务精度再再提高的情况下,也无法保证它的执行的顺序与时间是一致的。

工作上发生过与物理时间相关的问题

问题1: 发生时间跳变后,导致任务的调度器不再调度任务的情况

现象的描述: 
调度器默认应该定时把任务调度到线程上面,但是查看日志后发现任务并没有被调度   
原因解释:    
时间发生了向后的跳变,但是跳变的幅度大于调度的间隔,导致系统中认为调度成功,之后就没有追踪下面的状态(因为当时那部分代码的实现是再调度的时候修改调度的时间为last调度时间)    
解决方法:    
把时间从墙上时间更换为单调时间,然后添加检查执行的间隔即可。  

问题2:同步时间后,由于时间差溢出导致结果溢出的问题

现象描述:
从Ceph Mgr获取单个采集数据的情况下,然后发现数据变得异常的大,是一个int64是随机数
原因解释:
因为采用的是墙上的时候,但是发生了时间的向后调变,导致计算出来的值发生异常
解决方法:
把墙上时间更换为单调时间

逻辑时钟

做法: 需要一个全局的单调递增的ID。
但是是否可以使用同步后的时间来做呢?
答: 不可以。因为还是时间精度的不确定性。

节点本身的问题(硬件和软件)

比较极端但是可能出现率比较高的场景是进程出现暂停的情况

  1. 带GC的编程语言的GC(Garbage collection) halt
  2. 虚拟化环境的虚拟机的暂停
  3. 电脑发生休眠或者断电
  4. 上下文切换的情况下卡住了
  5. 执行同步磁盘的操作的时候(如fsync操作)
  6. 接收到信号导致进程退出的情况下

#在不可靠硬件构建可靠软件需要满足的条件和假设

此部分为DDIA第8章的描述总结,可以理解成为读书笔记

真相由多数决定(重要的判定条件)

节点不能通过自己的信息来判断自身的状态。因为节点可能出现失效、假死、甚至无法恢复的状况.可能出现的场景如下

1
2
3
1. 节点成功收到请求,但是因为路由问题,无法进行返回,超时后而被判失效
2. 半断开的节点发现自己的信息没有被其他节点所确认,意识到自己网络出现问题
3. GC halt导致无法接受请求,但是对于应用来说,切回到工作状态时候,自身状态没有变化

以上的多种情景都是实际出现问题,但是自身的状态并不会改变,因为没有收到其他节点的通知,它会一直维持原来的状态。
所以分布式的算法依赖节点间的相互投票与最小投票数(使用Quorum机制,m>=(n+1)/2)进行对比来产生结果,由于一个集群不会存在两个多数在同事做出相互冲突的决定,减少对特定节点的依赖。

基于上面所提及节点不能通过自己的信息来判断自己的状态,我们讨论一个比较常见的问题,主节点与锁是比较常见的使用方法

1
2
3
可能的出现的场景
1. 只允许一个节点作为数据库分区的主节点(Multi-Raft是其中的一种实现)
2. 只允许一个事务或者客户端持有特定资源的锁,以防止同时并发写入带来的数据被破坏的问题

如果由自己的状态决定可能会出现的问题

但是此处我们的假设时在分布式系统中,那么有可能出现节点中间出现了角色的切换,但是如果不加以对状态的判断,可能会出现这样的问题
问题描述

客户端写入存储的时候需要先从分布式服务中获取锁,有两个客户端在线,客户端A先从服务中获取锁,然后发生了进程halt状态(可能是GC或者其他的原因)
如果不加判断的情况下,在客户端A halt的状态的情况下,客户端B获得锁并且写入存储成功,然后客户端A从Halt状态中回复正常,客户端A认为锁还在,然后写入存储,
如果两个客户端写入的是同一个文件的话,数据可能会出现问题。

解决上面的问题

对于上面的问题,我们可以使用一个Fencing令牌来解决这个问题。(此机制到后面的文章还会继续详细讲解)

我们在客户端A获取锁的时候给令牌赋予一个值,为33。 在客户端B获取锁的时候赋予一个令牌值34。
当B写入完成后,存储会把已写入令牌改为34,下一次分发的令牌为35, 这样的话,如果A带上33的令牌写入的时候直接被拒绝,能够保证一致性。
实际上上面这种方法不仅加上了一个令牌,而且在服务器端会有一个关于令牌的校验。这种做法非常常见。

理论系统模型与现实

对于一个在现实中能够容错的分布式系统,我们需要对预期的系统错误证明形式化的描述。我们通过定义部分的系统模型来形式化描述算法的前提条件。

计时模型

#### 同步模型 假定有上界网络延迟和上界进程的暂停和上界时钟误差。(但是这个模型一般不存在,因为以太网的不可靠性,除非使用更加稳定的网络介质,并且使用高精度时间设备同步) #### 部分同步模型 大多数情况下像一个同步系统运行,但是会有超出网络延迟,进程暂停和时钟漂移的上界。这个就是比较现实的模型,就是上面我们提到的真实环境可能出现的情况的总和。而且大部分时间比较稳定,小部分时间会有背离,但是发生背离就会出现三者中的组合的情况出现。 #### 异步模型

节点失效模型

#### 崩溃-中止模型 节点以一种特定的方式发生故障,故障后,出现系统崩溃,并且节点不会再添加进入集群中,无法恢复 #### 崩溃-恢复模型 节点可能会在任何时候发生崩溃,并且会在未知的时间长度来进行恢复。但是节点上只要持久性存储的数据都可以在恢复之后得以保存,内存的状态会消失。 #### 拜占庭失效 在非可信任的集群中进行共识的计算,可能会出现作弊、欺诈的操作(区块链上面的假设的环境) 所以满足我们实际生产环境(互联网或者企业内部的环境)的是 部分同步模型+ 崩溃恢复模型的组合。

我们目前的分布式的环境是 崩溃-恢复模型+ 部分同步的模型

分布式共识算法的正确性必须要有以下的性质

  1. 唯一性
    两个令牌不能获得相同的值
  2. 单调递增
    如果请求x返回令牌tx, 请求y返回了令牌ty, 且x在y开始之前先完成,tx < ty
  3. 可用性
    请求令牌的节点如果不发生崩溃最终一定能收到响应(无论是对应用来说正确或者错误的回答)

分布式共识算法的安全性和活性

在正确性中其实可以再进行细分分为安全性和活性(理解上,并非准确的定义,准确的定义可以查看FLP的理论):
安全性: 没有发生意外
活性: 预期的事情一定会发生(最终会发生)

如果违反了安全的属性,我们可以找到发生特定时间的时间点。但是违反安全属性的事件发生,必定不可逆,数据丢失。
活性的话无法明确具体发生的时间点,但是希望再未来某个时间点能够达到要求。

ShareNote

PS: 文章中部分的图来源是来自于《数据密集型应用系统设计》,为PDF翻译的截图,截图的引用在此处。如果涉及到版权的问题,请通知我,我会使用工具继续重画。

  1. IBM Defination of Distributed System
  2. 数据密集型应用系统设计
  3. FLP Impossibility
  4. 拜占庭错误
  5. 拜占庭容错
  6. Riak