分布式系统对于刚刚入门作研发的程序员还是接触不多,刚刚入行的大部分程序员做的最多工作还是开发单体应用和简单业务操作,遇到最多问题估计也是本地线程并发数据安全问题,和数据状态一致性问题。而往后发展最多把单体应用部署为 HA 主从节点的方式备份架构,采用 nginx 做网络应用层的负载均衡。单体应用 bug 修复往往是万能法重启应用,让应用的状态消失,再恢复到正常状态来解决问题,但是实际上问题还是没有得到解决,除非是一些硬件故障可以跟换硬件,软件本身的问题需要开发者自己修复发布新的版本才能行。


分布式系统

而分布式系统是由多个独立计算机组成的集合,而大型的软件系统被拆分成了多个部分,部署在网络中计算机中,这些计算机通过网络互相通信协同工作,在设计分布式系统时,需要考虑以下几个问题:

  • 通信协议:如何让分布式系统中的各个节点进行通信?需要确定适合该系统的通信协议,并确保节点之间的通信稳定可靠。
  • 数据一致性:如何确保分布式系统中的所有节点都拥有相同的数据副本?需要选择适合该系统的数据一致性协议,如Paxos或Raft等,并在设计时确保数据的一致性。
  • 负载均衡:如何在分布式系统中均衡地分配负载?需要设计适合该系统的负载均衡策略,并确保在节点故障或网络问题时系统依然可用。
  • 安全性:如何保护分布式系统中的数据和节点不受攻击?需要在设计时考虑安全问题,并实现适当的安全措施,如加密和身份验证等。
  • 可扩展性:如何在需要时扩展分布式系统的规模?需要设计可扩展的系统架构,以便可以添加更多的节点和处理更多的请求。
  • 故障恢复:如何在节点故障或网络问题时恢复分布式系统的功能?需要设计故障恢复机制,并确保系统在出现问题时可以快速自动恢复。
  • 性能优化:如何优化分布式系统的性能?需要考虑各个节点的性能特点,并尝试优化通信协议、数据一致性协议、负载均衡策略等,以获得最佳的性能表现。

网络和时间是设计分布式系统时必须考虑的关键因素,网络延迟、带宽限制和数据包丢失等问题可能导致节点之间的通信出现问题,从而影响系统的性能和可靠性。时间同步也是分布式系统设计中非常重要的问题,因为节点之间的时钟可能会出现偏差,导致数据一致性和协议的正确性受到影响,因此,在设计分布式系统时,必须考虑网络和时间问题,并采取适当的措施来解决这些问题。目前时间存在于物理世界中,一个系统可能被部署在全球的网络服务器节点中,这些地区的用户时区也不一样,而且是还是多应用进行异步操作的,此时分布式系统设计就不单单是单体系统中加个锁就能解决的问题了。


扩展性

对于单体系统数据的一致性也是一个问题,例如数据库和缓存系统的数据一致性问题,当然缓存系统的数据一致性存在时间窗口的偏差是可以容忍的,但是对于分布式系统的数据一致性要考虑到问题加很多,关系型的数据库的 ACID 已经帮业务层解决了很多问题,而分布式系统特别是针对的分布式数据库,数据被分散在不同网络节点上,如何处理 ACID ?目前很多微服务架构是使用一些数据库中间件代理解决,或者说分库分表,而颠覆性的是 NewSQL 数据库例如 PingCAP 和 Amazon Aurora 分布式关系数据库来解决分布式数据一致性系统。

分布式关系数据库用起来来和传统的 Oracle 数据库是没有区别的,对于业务层是无感知的,但是分布式数据库本身的设计问题是要对 CAP 定律进行权衡的,大部分采用的分区同步复制写入的方式,而这些数据如何保证一致性?总的来说,NewSQL 是一种结合了传统关系型数据库 ACID 的优点,并加入了一些新技术,以提高数据库的性能和可伸缩性。

为提高 CAP 定律中的 P 容错机制,通常采用的方法是将数据分片复制到多个节点上,以提高数据的可靠性和容错性,同时还需要考虑 CAP 中的 A 可用机制,对节点故障进行监测和自动故障转移处理,保证系统的高可用性,NoSQL 系列例如 Redis 采用的是一致性哈希算法,Facebook Cassandra 也是一致性哈希算法,这种采用 Gossip 协议进行数据同步的,但是数据还是分区存储在当个节点上,遇到 ACID 需求无法将整个集群视为一个整体系统做强一致性数据处理,只能在单个数据节点上做 ACID 处理,并且这种分区有热点数据问题。


一致性

为了保证分区所有节点数据一致性目前大多数数据库采用的是同步复制,换句话来说往集群中一个节点写入数据其他节点马上会同步到最新的数据,但是由于分布式系统是建立于在不可靠网络之上的,如果出现分布式分区脑裂情况,导致多个分区不能通常通信,此时数据版本该从何处写入,从何处复制到集群中剩下的可以节点中?目前大多数 NewSQL 采用两种分布式算法解决的,Raft 和 Paxos 算法。

Paxos 和 Raft 这两种协议的目标都是在分布式系统中实现一致性,保证分布式系统中的各个节点的数据一致性。这里 Raft 算法在工业级使用的是最多的,它采用领导者选举的方式,保证系统中的多个节点之间的数据一致性。相对于 Paxos 协议,Raft 协议的实现较为简单,而且容错性更好。

如何让多个分区中的节点数据保存一致?在不同时间下操作的客户端如何保证一致?时间相对于客户端来说对于自己是相对的,而对于服务器来说是要面临多个客户端时间线的,整个世界都是异步的,如何多个分区保证数据一致性?分区出现了脑裂怎么办?在 Raft 协议中把这些抽象成了 3 个部分,第一个数据采用日志的形式同步,第二个多台服务器之间传输数据听谁的?Raft 采用的是选举机制,从多个节点中选择一个领导节点,第三个容错机制采用多节点之间心跳机制来保证,和我之前设计的 web session 那篇文章的超时机制类似。

在 Raft 算法通过日志复制来保证系统中各个节点之间的数据一致性,领导者会向其他节点发送日志条目,其他节点会将这些日志条目添加到自己的日志中,并向领导者发送确认消息,当所有节点将日志条目持久化并且发送确认消息之后则认为这条数据状态一致性得到保持,至于实现细节可以查看文末,我列出的 Raft 协议相关文章。


不可抗拒因素

分布式系统产生脑裂的原因最多原因就是网络问题,网络层的 IP 协议并不能保证网络数据包传输过程中保证数据包正常无误的传输,会出现丢包、数据包网络延迟、重新发送数据包,这些问题在 TCP 协议得到解决,包括数据包顺序问题也得到解决,但是 TCP 不能消除网络延迟问题。延迟是基于时间的东西,这是不可能得到解决,除非有时间机器,分布式系统脑裂出现原因一部分也是因为数据包延迟达到而产生了,或者请求响应问题产生的。能让程序产生延迟现象的原因有很多,下面 3 种都是归根结底是时间带来的问题:

  • 网络分区
  • 垃圾回收
  • 基于时间

对于分布式系统网络故障,目前最常见的解决方法为负载均衡、主从复制,这些典型案例为 web 应用服务器通过 nginx 做负载均衡,一台服务器出现了故障之后另外一台服务器还能正常工作;主从架构典型应用为 Redis 哨兵模式和 Linux 网络中所有的多网卡绑定设计,都是用来解决网络故障引起系统故障。

解决上面这种网络故障常见,最常用的办法为心跳检查,轮询的方式检查健康,但是别忘了检测服务器和故障服务器之间也是通过网络连接,这样网络分区问题又回归到本身。

在开发单体应用的 web 的时候会对 http 请求延迟做测试,如果延迟过长客户端会无法加载出来数据,而这些问题也是数据包在网络中传输延迟导致的。同理在分布式中检查故障的手段也是基于时间和超时控制的,如何设置超时时间窗口又带来一个新的问题?设置过短导致网络频繁,设置过长又导致出现了故障不能立马检查到。

导致相应延长原因也有很多,网络数据包链路延迟,或者服务器程序进程崩溃,进程处理数据堆积压力过大,特别是服务进程已经处于高负荷状态,导致无响应,检测端如何应对这种情况?网络数据拥堵问题和现实生活中道路拥堵一样,一段路程原本能在 d 时间内跑完,而出现拥堵情况则是不确定的,出现网络拥堵情况有:

  • 交换机故障
  • 程序高负载
  • 虚拟化环境 CPU 切换资源延迟
  • 基于有 GC 的语言出现 STW 情况

并且 TCP 有数据包默认的超时机制来重新发送数据包,如果数据包在交换机中堆积,而 TCP 的超时机制会认为数据包已经丢失,导致数据包重新发送,这样以来本来网络就够拥堵的了,再重新发送数据包会更加堵塞,跟会加深网络的延迟影响。在公有云例如 AWS 中多个物理服务器公用一台交换机的情况,网络的不可抗拒因素又会提高。

假设现在不同考虑到网络可靠性问题了,数据包能在规定的时间内到达分区节点中,现在剩下的问题为服务器本身和应用程序本身的问题,不同的时区的分区节点如何做时间的协调工作,这种最为常见解决方法是通过时间戳的方式,例如两个客户端同时向分布式数据库写入一条数据,谁会优达到呢?这很难受,取决于服务端如何处理,例如服务器的程序正在做 GC 垃圾回收,导致整个进程出现了 STW 状态,当进程苏醒时时间就会存在偏差了。

基于时间带来的分布式问题,这个研究方向太多了,而且时间戳也有 Long 值不能表示的那一天,这让我想起了,存在看到一个科幻的故事,说一个未来人从未来回到过去,寻找 IBM5100 机器,以此来解决计算机世界中的时间问题。目前分布式数据中心的数据库 Google 采用的 GPS 和原子钟的方式解决,时间同步的问题。

针对于进程内的 GC 导致程序停止问题,和磁盘 IO 读写导致程序阻塞问题影响的涉及时间偏差问题,目前解决方案是使用无 GC 编程语言,或者对 GC 参数做设置,例如 Java 最新版本采用 ZGC 设计方案,或者使用 Rust 这种特殊内存管理的编程语言进行编写应用,针对分布式常见可以采用滚动停机方法进行 GC 回收,当要进行垃圾回收时把执行任务节点从集群剔除,这也是一种常见的做法。

如果你对实现性要求非常高的场景,例如汽车和飞机的各个传感器,对应时间响应要求要为试试的,那么有 GC 的编程语言就不是很好的选择了,可以使用 C 语言或者现在流行的 Rust 编程语言,RTOS 系统之类软件,例如汽车在发生碰撞的那一刻,安全气囊要即时打开,系统肯定不希望 GC 暂停导致无法及时释放安全气囊导致人生安全的事故。


不可靠因素太多,怎么确定集群中的数据分区问题?故障检查问题?单靠节点自身来检查是完全不够的,而且网络波动导致分区故障,在同一个地区服务器可能会检查到,其他地区服务器因为网络线路原因导致不能监测到,目前最好办法为法定人数来解决,多个节点对一个节点做仲裁,如果集群中的三分之二节点认为某个节点挂了,那么它就真的挂了,即使它进程正在做 GC 垃圾回收处于假死状态,也等同于从集群中移除废除它的工作职责,目前实现这些算法的常用的为 raft 协议和 quorum 协议,以法定人数来解决这些问题。


其他资料

便宜 VPS vultr
最后修改:2023 年 07 月 05 日
如果觉得我的文章对你有用,请随意赞赏 🌹 谢谢 !