分布式核心设计原则

目录

1、软件架构设计的六大原则

1. 单一职责原则(Single Responsibility Principle - SRP)

2. 开放封闭原则(Open Closed Principle - OCP)

3. 里氏替换原则(Liskov Substitution Principle - LSP)

4. 最少知识原则(Least Knowledge Principle - LKP)

5. 接口隔离原则(Interface Segregation Principle - ISP)

6. 依赖倒置原则(Dependence Inversion Principle - DIP)

补充设计原则

2、软件架构需要考虑的基本原则

3、分布式系统设计理念

4、分布式理论(一) -CAP原则

5、CAP原则论证

6、CAP原则权衡

7、分布式理论(二) - BASE理论


1、软件架构设计的六大原则

1. 单一职责原则(Single Responsibility Principle - SRP)

原文:There should never be more than one reason for a class to change.

译文:永远不应该有多于一个原因来改变某个类。

理解:对于一个类而言,应该仅有一个引起它变化的原因。说白了就是,不同的类具备不同的职责,各施其责。这就好比一个团队,大家分工协作,互不影响,各做各的事情。

应用:当我们做系统设计时,如果发现有一个类拥有了两种的职责,那就问自己一个问题:可以将这个类分成两个类吗?如果真的有必要,那就分吧。千万不要让一个类干的事情太多!

2. 开放封闭原则(Open Closed Principle - OCP)

原文:Software entities like classes, modules and functions should be open for extension but closed for modifications.

译文:软件实体,如:类、模块与函数,对于扩展应该是开放的,但对于修改应该是封闭的。

理解:简言之,对扩展开放,对修改封闭。换句话说,可以去扩展类,但不要去修改类。

应用:当需求有改动,要修改代码了,此时您要做的是,尽量用继承或组合的方式来扩展类的功能,而不是直接修改类的代码。当然,如果能够确保对整体架构不会产生任何影响,那么也没必要搞得那么复杂了,直接改这个类吧。

3. 里氏替换原则(Liskov Substitution Principle - LSP)

原文:Functions that use pointers or references to base classes must be able to use objects of derived classes without knowing it.

译文:使用基类的指针或引用的函数,必须是在不知情的情况下,能够使用派生类的对象。

理解:父类能够替换子类,但子类不一定能替换父类。也就是说,在代码中可以将父类全部替换为子类,程序不会报错,也不会在运行时出现任何异常,但反过来却不一定成立。

应用:在继承类时,务必重写(Override)父类中所有的方法,尤其需要注意父类的 protected 方法(它们往往是让您重写的),子类尽量不要暴露自己的 public 方法供外界调用。

4. 最少知识原则(Least Knowledge Principle - LKP)

原文:Only talk to you immediate friends.

译文:只与你最直接的朋友交流。

理解:尽量减少对象之间的交互,从而减小类之间的耦合。简言之,一定要做到:低耦合,高内聚。

应用:在做系统设计时,不要让一个类依赖于太多的其他类,需尽量减小依赖关系,否则,您死都不知道自己怎么死的。

5. 接口隔离原则(Interface Segregation Principle - ISP)

原文:The dependency of one class to another one should depend on the smallest possible interface.

译文:一个类与另一个类之间的依赖性,应该依赖于尽可能小的接口。

理解:不要对外暴露没有实际意义的接口。也就是说,接口是给别人调用的,那就不要去为难别人了,尽可能保证接口的实用性吧。她好,我也好。

应用:当需要对外暴露接口时,需要再三斟酌,如果真的没有必要对外提供的,就删了吧。一旦您提供了,就意味着,您将来要多做一件事情,何苦要给自己找事做呢。

6. 依赖倒置原则(Dependence Inversion Principle - DIP)

原文:High level modules should not depends upon low level modules. Both should depend upon abstractions. Abstractions should not depend upon details. Details should depend upon abstractions.

译文:高层模块不应该依赖于低层模块,它们应该依赖于抽象。抽象不应该依赖于细节,细节应该依赖于抽象。

理解:应该面向接口编程,不应该面向实现类编程。面向实现类编程,相当于就是论事,那是正向依赖(正常人思维);面向接口编程,相当于通过事物表象来看本质,那是反向依赖,即依赖倒置(程序员思维)。

应用:并不是说,所有的类都要有一个对应的接口,而是说,如果有接口,那就尽量使用接口来编程吧。

将以上六大原则的英文首字母拼在一起就是 SOLID(稳定的),所以也称之为 SOLID 原则。只有满足了这六大原则,才能设计出稳定的软件架构!但它们毕竟只是原则,只是四人帮给我们的建议,有些时候我们还是要学会灵活应变,千万不要生搬硬套,否则只会把简单问题复杂化

补充设计原则


1. 组合/聚合复用原则(Composition/Aggregation Reuse Principle - CARP)

当要扩展类的功能时,优先考虑使用组合,而不是继承。这条原则在 23 种经典设计模式中频繁使用,如:代理模式、装饰模式、适配器模式等。可见江湖地位非常之高!

2. 无环依赖原则(Acyclic Dependencies Principle - ADP)

当 A 模块依赖于 B 模块,B 模块依赖于 C 模块,C 依赖于 A 模块,此时将出现循环依赖。在设计中应该避免这个问题,可通过引入“中介者模式”解决该问题。

3. 共同封装原则(Common Closure Principle - CCP)

应该将易变的类放在同一个包里,将变化隔离出来。该原则是“开放-封闭原则”的延生。

4. 共同重用原则(Common Reuse Principle - CRP)

如果重用了包中的一个类,那么也就相当于重用了包中的所有类,我们要尽可能减小包的大小。

5. 好莱坞原则(Hollywood Principle - HP)

好莱坞明星的经纪人一般都很忙,他们不想被打扰,往往会说:Don't call me, I'll call you. 翻译为:不要联系我,我会联系你。对应于软件设计而言,最著名的就是“控制反转”(或称为“依赖注入”),我们不需要在代码中主动的创建对象,而是由容器帮我们来创建并管理这些对象。

其它设计原则
1. 不要重复你自己(Don't repeat yourself - DRY)

不要让重复的代码到处都是,要让它们足够的重用,所以要尽可能地封装。

2. 保持它简单与傻瓜(Keep it simple and stupid - KISS)

不要让系统变得复杂,界面简洁,功能实用,操作方便,要让它足够的简单,足够的傻瓜。

3. 高内聚与低耦合(High Cohesion and Low Coupling - HCLC)

模块内部需要做到内聚度高,模块之间需要做到耦合度低。

4. 惯例优于配置(Convention over Configuration - COC)

尽量让惯例来减少配置,这样才能提高开发效率,尽量做到“零配置”。很多开发框架都是这样做的。

5. 命令查询分离(Command Query Separation - CQS)

在定义接口时,要做到哪些是命令,哪些是查询,要将它们分离,而不要揉到一起。

6. 关注点分离(Separation of Concerns - SOC)

将一个复杂的问题分离为多个简单的问题,然后逐个解决这些简单的问题,那么这个复杂的问题就解决了。难就难在如何进行分离。

7. 契约式设计(Design by Contract - DBC)

模块或系统之间的交互,都是基于契约(接口或抽象)的,而不要依赖于具体实现。该原则建议我们要面向契约编程。

8. 你不需要它(You aren't gonna need it - YAGNI)

不要一开始就把系统设计得非常复杂,不要陷入“过度设计”的深渊。应该让系统足够的简单,而却又不失扩展性,这是其中的难点。
 

2、软件架构需要考虑的基本原则

1、稳定性原则

架构尽可能的简单,清晰,不过度设计。

2、注意隔离处理

稳定业务和易变业务要分离处理,核心业务和非核心业务要分离处理,电商业务和辅助流程要分离,应用和数据要分离,服务和实现细节分离,前台和后台分离。

3、抽象化

应用只依赖于服务抽象,不依赖服务实现细节。
应用只依赖逻辑数据库,不关心具体的数据库位置和分片。
应用虚拟化部署,不关心实体机配置,动态调配资源。
4、松耦合

跨域调用尽量异步化,非核心业务尽量异步化,必须同步调用时,要设置超时时间和重试机制。

5、容灾设计

服务要自治,彼此能够独立修改、部署、发布和管理,避免引起连锁反应。
通过集群容错,应用系统集群,避免单点。
多机房容灾部署,多活机制,避免出现单机房崩溃情况。
6、什么是架构师

架构师首先是对技术和业务都全面了解的系统设计师,架构师可以不用写具体的业务实现逻辑,但是他必须对系统整体采用的技术及其业务流转过程非常熟悉,这样才能够根据实际业务需要给出最合适的架构设计。最好的架构不一定适合业务,适合自己的才是最好的。

其次,架构师应该密切关注技术前沿,能够采用一些新的技术对当前繁杂的业务进行变革,从这个角度上讲,架构师又承担着变革者的角色。举一个很简单的例子,一个常规的项目,如果没有架构师,经过半年时间也能设计完成。但中间可能会经历很多不必要的重复劳动工作,而且最终产品的稳定性可能欠佳。如果有一个称职的架构师参与,这个系统可能三个月就完成,期间的重复劳动可能会减少,同时最终产品的稳定性应该也有所保障。架构师更多的工作应该是预见未来,多做一些防患于未然的工作。

当前技术团队的人员众多,根据管理学上管理幅度的理论,一个管理者管理8-15个人比较合理,这样既可以有足够的精力思考公司战略,达成业务,同时又有精力去培养人才。
 

3、分布式系统设计理念

首先,分布式系统的首要目的是提升系统的整体性能和吞吐量。如果最终设计出来的分布式系统占用了10台机器才勉强达到单机系统的两倍性能,那么这个分布式系统还有存在的价值吗?另外,即使采用了分布式架构,也仍然需要尽力提升单机上的程序性能,使得整体性能达到最高。所以,我们仍然需要掌握高性能单机程序的设计和编程技巧,例如多线程编程、多进程高性能IPC通信、高性能的网络框架等。

其次,任何分布式系统都存在让人无法回避的风险和严重问题,即系统发生故障的概率大大增加:小到一个服务器的硬盘发生故障或宕机、一根网线被老鼠啃坏了,大到交换机甚至几十台服务器一起歇火。在分布式系统下故障概率之所以增加,除了主要由于网络通信天生的不可靠性及物理上的分布部署,还由于X86服务器的品质越来越差,远不如UNIX小机器,这大概是工业化导致“工匠精神”的匮乏在IT上的一个缩影吧。

综上分析,我们看到分布式系统社二级的两大关键目标是“性能”与“容错性”,而这两个目标的实现恰恰都是很棘手的问题,而且相互羁绊!举个例子,比如我们设计一个分布式存储系统,处于对性能的考虑,写文件时先写一个副本到某个机器上并立即返回,然后异步发起多副本的复制过程,这种设计性能最好,但存在“容错性”的风险,即文件写完后,目标机器立即发生故障,导致文件丢失!如果同时写多个副本,每个副本成功以后再返回,则又导致“性能”下降,因为这个过程取决于最慢的那台机器的性能。

由于“性能”的指标是绝对的,而“容错性”的指标是绝对的,而且实际上对于不同的数据与业务,我们要求的容错性其实可以存在很大的差异:允许意外丢失一些日志类的数据;允许一些信息类的数据暂时不一致而最终达到一致;而对交易类的数据则要求有很高的可靠性。于是你会发现,很多分布式系统的设计都提供了多种容错性策略,以适应不同的业务场景,我们在学习和设计分布式系统的过程中也需要注意这一特性。

分布式系统设计中的两大思路:中心化和去中心化。

中心化

中心化的设计思想很简单,分布式集群中的节点机器按照角色分工,大体上分为两种角色:”领导“和”干活儿的“,”领导“通常负责分发任务并监督”干活的“,发现谁太闲,就想方设法地给其安排新任务,确保没有一个”干活的“能够偷懒;如果”领导“发现某个”干活的“因为劳累过度而病倒了,则是不会考虑先尝试”医治“他的,而是一脚踢出去,然后把他的任务分给其他人。而Google最近开源的基于容器技术的微服务架构Kubernetes就恰好采用了这一设计思路。

在分布式中心化的设计思想中,除了上诉的设计思路之外,还有一种设计思路与编程中的敏捷开发的做法类似,即充分相信每个“干活儿的”,“领导”只负责任务的生成而不在指派任务,由每个“干活的”自发去领任务,从而避免让个别员工积劳成疾,并鼓励能者多劳,但还是不发奖金。

中心化的设计存在的最大问题就是“领导”的安危问题,如果“领导”出了问题,则群龙无首,整个系统集群就会奔溃。但我们难以同时安排两个“领导”以避免单点问题,是因为“一山不容二虎”。为了解决这个问题,大多数中心化系统都采用了主备两个“领导”的设计方案,可以是热备或者冷备,也可以是自动切换或者手动切换,而且越来越多的新系统都开始具备自动选举切换“领导”的能力,以提升系统的可用性。中心化设计还存在另外一个潜在的问题就是“领导”的能力问题:可以领导10个人高效工作并不意味着可以领导100个人高效工作,所以如果系统设计和实现得不好,问题就会卡在“领导”身上。

去中心化

在去中心化的设计里,通常没有“领导”和“干活儿的”这两种角色的区分,大家的角色都是一样的,地位是平等的。去中心化设计的核心在于整个分布式系统中不存在区别于其他节点的“领导”,因此不存在单点故障问题,但由于不存在“领导”,所以每个节点都需要跟其他节点对话才能获取到必要的信息,而分布式系统通信的不可靠性,则大大增加了上诉功能的实现难度。

去中心化最难解决的问题就是“脑裂”问题,这种情况的发生率很低,但影响很大。脑裂指一个集群由于网络的故障,被分为至少两个彼此无法通信的单独集群,此时如果两个集群都各自工作,则可能会产生严重的数据冲突和错误。一般的设计思路是,当集群判断发生了脑裂问题,规模较小的集群就“自杀”或者拒绝服务。

实际上,完全意义的真正去中心化的分布式系统并不多见。相反,外部看来去中心化但工作机制采用了中心化设计思想的分布式系统正在不断涌出。在这种架构下,集群中的领导是被动态选择出来的,而不是人为预先指定的,而且集群发生故障的情况下,集群的成员会自发地举行“会议”选举新的“领导”主持工作。最典型的案例就是Zookeeper。


4、分布式理论(一) -CAP原则

Consistency(一致性):指数据在多个副本之间能够保持一致的特性(严格的一致性)
Availability(可用性):指系统提供的服务必须一直处于可用的状态,每次请求都能获取到非错的响应(不保证获取的数据为最新数据)
Partition tolerance(分区容错性):分布式系统在遇到任何网络分区故障的时候,仍然能够对外提供满足一致性和可用性的服务,除非整个网络环境都发生了故障

什么是分区?

在分布式系统中,不同的节点分布在不同的子网络中,由于一些特殊的原因,这些子节点之间出现了网络不通的状态,但他们的内部子网络是正常的。从而导致了整个系统的环境被切分成了若干个孤立的区域,这就是分区。

5、CAP原则论证

如图所示,是我们证明CAP的基本场景,网络中有两个节点N1和N2,可以简单的理解N1和N2分别是两台计算机,他们之间网络可以连通,N1中有一个应用程序A,和一个数据库V,N2也有一个应用程序B和一个数据库V。现在,A和B是分布式系统的两个部分,V是分布式系统的数据存储的两个子数据库。

  • 在满足一致性的时候,N1和N2中的数据是一样的,V0=V0。
  • 在满足可用性的时候,用户不管是请求N1或者N2,都会得到立即响应。
  • 在满足分区容错性的情况下,N1和N2有任何一方宕机,或者网络不通的时候,都不会影响N1和N2彼此之间的正常运作。

如图所示,这是分布式系统正常运转的流程,用户向N1机器请求数据更新,程序A更新数据库V0为V1。分布式系统将数据进行同步操作M,将V1同步的N2中V0,使得N2中的数据V0也更新为V1,N2中的数据再响应N2的请求。

根据CAP原则定义,系统的一致性、可用性和分区容错性细分如下:

  • 一致性:N1和N2的数据库V之间的数据是否完全一样。
  • 可用性:N1和N2的对外部的请求能否做出正常的响应。
  • 分区容错性:N1和N2之间的网络是否互通。

这是正常运作的场景,也是理想的场景。作为一个分布式系统,它和单机系统的最大区别,就在于网络。现在假设一种极端情况,N1和N2之间的网络断开了,我们要支持这种网络异常。相当于要满足分区容错性,能不能同时满足一致性和可用性呢?还是说要对他们进行取舍?

假设在N1和N2之间网络断开的时候,有用户向N1发送数据更新请求,那N1中的数据V0将被更新为V1。由于网络是断开的,所以分布式系统同步操作M,所以N2中的数据依旧是V0。这个时候,有用户向N2发送数据读取请求,由于数据还没有进行同步,应用程序没办法立即给用户返回最新的数据V1,怎么办呢?

这里有两种选择:

第一:牺牲数据一致性,保证可用性。响应旧的数据V0给用户。
第二:牺牲可用性,保证数据一致性。阻塞等待,直到网络连接恢复,数据更新操作M完成之后,再给用户响应最新的数据V1。
这个过程,证明了要满足分区容错性的分布式系统,只能在一致性和可用性两者中,选择其中一个。

6、CAP原则权衡

6.1. CA without P
如果不要求P(不允许分区),则C(强一致性)和A(可用性)是可以保证的。但其实分区不是你想不想的问题,而是始终会存在,因此CA的系统更多的是允许分区后各子系统依然保持CA。

6.2. CP without A
如果不要求A(可用),相当于每个请求都需要在Server之间强一致,而P(分区)会导致同步时间无限延长,如此CP也是可以保证的。很多传统的数据库分布式事务都属于这种模式。

6.3. AP wihtout C
要高可用并允许分区,则需放弃一致性。一旦分区发生,节点之间可能会失去联系,为了高可用,每个节点只能用本地数据提供服务,而这样会导致全局数据的不一致性。现在众多的NoSQL都属于此类。

小结
对于多数大型互联网应用的场景,主机众多、部署分散。而且现在的集群规模越来越大,所以节点故障、网络故障是常态。这种应用一般要保证服务可用性达到N个9,即保证P和A,只有舍弃C(退而求其次保证最终一致性)。虽然某些地方会影响客户体验,但没达到造成用户流程的严重程度。

对于涉及到钱财这样不能有一丝让步的场景,C必须保证。网络发生故障宁可停止服务,这是保证CA,舍弃P。貌似这几年国内银行业发生了不下10起事故,但影响面不大,报到也不多,广大群众知道的少。还有一种是保证CP,舍弃A,例如网络故障时只读不写。

7、分布式理论(二) - BASE理论

前言
BASE理论是由eBay架构师提出的。BASE是对CAP中一致性和可用性权衡的结果,其来源于对大规模互联网分布式系统实践的总结,是基于CAP定律逐步演化而来。其核心思想是即使无法做到强一致性,但每个应用都可以根据自身业务特点,才用适当的方式来使系统打到最终一致性。

正文
1. CAP的3选2伪命题
实际上,不是为了P(分区容错性),必须在C(一致性)和A(可用性)之间任选其一。分区的情况很少出现,CAP在大多时间能够同时满足C和A。

对于分区存在或者探知其影响的情况下,需要提供一种预备策略做出处理:

探知分区的发生;
进入显示的分区模式,限制某些操作;
启动恢复过程,恢复数据一致性,补偿分区发生期间的错误。
2. BASE理论简介
BASE理论是Basically Available(基本可用),Soft State(软状态)和Eventually Consistent(最终一致性)三个短语的缩写。

其核心思想是:

既是无法做到强一致性(Strong consistency),但每个应用都可以根据自身的业务特点,采用适当的方式来使系统达到最终一致性(Eventual consistency)。

3. BASE理论的内容

  • 基本可用(Basically Available)
  • 软状态(Soft State)
  • 最终一致性(Eventually Consistent)

下面展开讨论:

3.1. 基本可用
什么是基本可用呢?假设系统,出现了不可预知的故障,但还是能用,相比较正常的系统而言:

响应时间上的损失:正常情况下的搜索引擎0.5秒即返回给用户结果,而基本可用的搜索引擎可以在2秒作用返回结果。

功能上的损失:在一个电商网站上,正常情况下,用户可以顺利完成每一笔订单。但是到了大促期间,为了保护购物系统的稳定性,部分消费者可能会被引导到一个降级页面。

3.2. 软状态
什么是软状态呢?相对于原子性而言,要求多个节点的数据副本都是一致的,这是一种“硬状态”。

软状态指的是:允许系统中的数据存在中间状态,并认为该状态不影响系统的整体可用性,即允许系统在多个不同节点的数据副本存在数据延时。

3.3. 最终一致性
上面说软状态,然后不可能一直是软状态,必须有个时间期限。在期限过后,应当保证所有副本保持数据一致性,从而达到数据的最终一致性。这个时间期限取决于网络延时、系统负载、数据复制方案设计等等因素。

而在实际工程实践中,最终一致性分为5种:

3.3.1. 因果一致性(Causal consistency)

因果一致性指的是:如果节点A在更新完某个数据后通知了节点B,那么节点B之后对该数据的访问和修改都是基于A更新后的值。于此同时,和节点A无因果关系的节点C的数据访问则没有这样的限制。

3.3.2. 读己之所写(Read your writes)

读己之所写指的是:节点A更新一个数据后,它自身总是能访问到自身更新过的最新值,而不会看到旧值。其实也算一种因果一致性。

3.3.3. 会话一致性(Session consistency)

会话一致性将对系统数据的访问过程框定在了一个会话当中:系统能保证在同一个有效的会话中实现 “读己之所写” 的一致性,也就是说,执行更新操作之后,客户端能够在同一个会话中始终读取到该数据项的最新值。

3.3.4. 单调读一致性(Monotonic read consistency)

单调读一致性指的是:如果一个节点从系统中读取出一个数据项的某个值后,那么系统对于该节点后续的任何数据访问都不应该返回更旧的值。

3.3.5. 单调写一致性(Monotonic write consistency)

单调写一致性指的是:一个系统要能够保证来自同一个节点的写操作被顺序的执行。

在实际的实践中,这5种系统往往会结合使用,以构建一个具有最终一致性的分布式系统。

实际上,不只是分布式系统使用最终一致性,关系型数据库在某个功能上,也是使用最终一致性的。比如备份,数据库的复制过程是需要时间的,这个复制过程中,业务读取到的值就是旧的。当然,最终还是达成了数据一致性。这也算是一个最终一致性的经典案例。
 

猜你喜欢

转载自blog.csdn.net/gangsijay888/article/details/88385013