软件架构原则

2022-02-19  本文已影响0人  淡如菊行如水

1、中间件尽量选择AP系统

CAP诠释

C(一致性):即所有节点在任意时间,被访问返回的数据完全一致。

从服务端的视角来看,就是在 Client 写入一个更新后,Server 端如何同步这个新值到整个系统,从而保证整个系统的这个数据都相同。

而从客户端的视角来看,则是并发访问时,在变更数据后,如何获取到最新值。

A(可用性):即服务集群总能够对用户的请求给予响应。

从服务端的视角来看,就是服务节点总能响应用户请求,不会吞噬、阻塞请求。

而从客户端视角来看,发出的请求总有响应,不会出现整个服务集群无法连接、超时、无响应的情况。

P(分区容错):即出现分区故障或分区间通信异常时,系统仍然要对外提供服务。

在分布式环境,每个服务节点都不是可靠的,不同服务节点之间的通信有可能出现问题。

当某些节点出现异常,或者某些节点与其他节点之间的通信出现异常时,整个系统就产生了分区问题。

从服务端的视角来看,出现节点故障、网络异常时,服务集群仍然能对外提供稳定服务,就是具有较好的分区容错性。从客户端视角来看,就是服务端的各种故障对自己透明。

CAP原则

CAP 原则指示3个要素最多只能同时实现两点,不可能三者兼顾,由于网络硬件肯定会出现延迟丢包等问题,但是在分布式系统中,我们必须保证部分网络通信问题不会导致整个服务器集群瘫痪,另外即使分成了多个区,当网络故障消除的时候,我们依然可以保证数据一致性,所以我们必须保证分区容错性;

至于剩下的一致性和可用性,我们需要二选一,但是鱼和熊掌不可兼得,假设我们选择一致性,那我们就不能让用户访问无法进行数据同步的机器,毕竟该机器上的数据和其他正常机器上的不一致,但是这样我们就丢弃了可用性;

假设我们选择可用性,那我们就可以让用户访问无法进行数据同步的服务器,虽然保证了可用性,但是我们无法保证数据一致性。

CAP 权衡

在通常的分布式系统中,为了保证数据的高可用,通常会将数据保留多个副本(Replica),网络分区是既成的现实,于是只能在可用性和一致性两者间做出选择。

CAP 理论关注的是在绝对情况下,在工程上,可用性和一致性并不是完全对立的,我们关注的往往是如何在保持相对一致性的前提下,提高系统的可用性。

CA 架构:不支持分区容错,只支持一致性和可用性。

不支持分区容错性,也就意味着不允许分区异常,设备、网络永远处于理想的可用状态,从而让整个分布式系统满足一致性和可用性。

但由于分布式系统是由众多节点通过网络通信连接构建的,设备故障、网络异常是客观存在的,而且分布的节点越多,范围越广,出现故障和异常的概率也越大,因此,对于分布式系统而言,分区容错 P 是无法避免的,如果避免了 P,只能把分布式系统回退到单机单实例系统。

CP 架构:因为分区容错 P 客观存在,即相当于放弃系统的可用性,换取一致性。

系统在遇到分区异常时,会持续阻塞整个服务,直到分区问题解决,才恢复对外服务,这样可以保证数据的一致性。

选择 CP 的业务场景比较多,特别是对数据一致性特别敏感的业务最为普遍。比如在支付交易领域,Hbase 等分布式数据库领域,都要优先保证数据的一致性,在出现网络异常时,系统就会暂停服务处理。

分布式系统中,用来分发及订阅元数据的 Zookeeper,也是选择优先保证 CP 的。因为数据的一致性是这些系统的基本要求,否则,银行系统 余额大量取现,数据库系统访问,随机返回新老数据都会引发一系列的严重问题。

AP 架构:由于分区容错 P 客观存在,即相当于放弃系统数据的一致性,换取可用性。

在系统遇到分区异常时,节点之间无法通信,数据处于不一致的状态,为了保证可用性,服务节点在收到用户请求后立即响应,那只能返回各自新老不同的数据。

这种舍弃一致性,而保证系统在分区异常下的可用性,在互联网系统中非常常见。比如微博多地部署,如果不同区域的网络中断,区域内的用户仍然发微博、相互评论和点赞,但暂时无法看到其他区域用户发布的新微博和互动状态。对于微信朋友圈也是类似。

还有如 12306 的火车购票系统,在节假日高峰期抢票时,偶尔也会遇到,反复看到某车次有余票,但每次真正点击购买时,却提示说没有余票。

这样,虽然很小一部分功能受限,但系统整体服务稳定,影响非常有限,相比 CP,用户体验会更佳。

CAP 问题及误区

CAP 理论极大的促进了分布式系统的发展,但随着分布式系统的演进,大家发现,其实 CAP 经典理论其实过于理想化,存在不少问题和误区。

首先,以互联网场景为例,大中型互联网系统,主机数量众多,而且多区域部署,每个区域有多个 IDC。

节点故障、网络异常,出现分区问题很常见,要保证用户体验,理论上必须保证服务的可用性,选择 AP,暂时牺牲数据的一致性,这是最佳的选择。

但是,当分区异常发生时,如果系统设计的不够良好,并不能简单的选择可用性或者一致性。例如,当分区发生时,如果一个区域的系统必须要访问另外一个区域的依赖子服务,才可以正常提供服务,而此时网络异常,无法访问异地的依赖子服务,这样就会导致服务的不可用,无法支持可用性。同时,对于数据的一致性,由于网络异常,无法保证数据的一致性,各区域数据暂时处于不一致的状态。在网络恢复后,由于待同步的数据众多且复杂,很容易出现不一致的问题,同时某些业务操作可能跟执行顺序有关,即便全部数据在不同区域间完成同步,但由于执行顺序不同,导致最后结果也会不一致。长期多次分区异常后,会累积导致大量的数据不一致,从而持续影响用户体验。

其次,在分布式系统中,分区问题肯定会发生,但却很少发生,或者说相对于稳定工作的时间,会很短且很小概率。当不存在分区时,不应该只选择 C 或者 A,而是可以同时提供一致性和可用性。

再次,同一个系统内,不同业务,同一个业务处理的不同阶段,在分区发生时,选择一致性和可用性的策略可能都不同。

比如前面讲的 12306 购票系统,车次查询功能会选择 AP,购票功能在查询阶段也选择 AP,但购票功能在支付阶段,则会选择 CP。因此,在系统架构或功能设计时,并不能简单选择 AP 或者 CP。

而且,系统实际运行中,对于 CAP 理论中的每个元素,实际并不都是非黑即白的。比如一致性,有强一致性,也有弱一致性,即便暂时大量数据不一致,在经历一段时间后,不一致数据会减少,不一致率会降低。又如可用性,系统可能会出现部分功能异常,其他功能正常,或者压力过大,只能支持部分用户的请求的情况。甚至分区也可以有一系列中间状态,区域网络完全中断的情况较少,但网络通信条件却可以在 0~100% 之间连续变化,而且系统内不同业务、不同功能、不同组件对分区还可以有不同的认知和设置。

最后,CAP 经典理论,没有考虑实际业务中网络延迟问题,延迟自始到终都存在,甚至分区异常P都可以看作一种延迟,而且这种延迟可以是任意时间,1 秒、1 分钟、1 小时、1 天都有可能,此时系统架构和功能设计时就要考虑,如何进行定义区分及如何应对。

2、WatchDog -- 看门狗

watchdog 是一款优秀的系统监控工具。普通情况下,它看似无关紧要,但却能在危机关头力挽狂澜。因为它能够在系统资源即将耗尽或即将崩溃时主动重启系统,避免由于硬件罢工而导致的被动重启或宕机造成的数据损失和业务损失。

 watchdog 实际上是一个用于系统主动重启的计时器,默认倒计时为60秒,系统在60秒内往 /dev/watchdog 设备中进行一次写操作,如果成功则归零计时器,重新倒计时。如果在60秒内,没有任何写操作,watchdog 便认为系统发生严重故障,主动重启系统,以求自救而不是等死。

watchdog 会根据配置检测系统平均负载,剩余内存,网络是否通畅等,为此我们可以轻松设置系统可用资源的上限,当满足某一条件则会触动重启机制,这可以方便的保护机器。

例如,我们设置当剩余虚拟内存页面低于x,或15分钟平均负载高于y时,系统将自动重启,配合heartbeat,可以把灾难降至最低,特别是高负载带来的系统宕机。

3、追问原始需求

原始需求最核心的要求不在于已经采纳的原始需求,恰恰在于被拒绝的原始需求。只有知道了哪些原始需求被拒绝了或者延迟了,才能相信所采纳的原始需求是真正要做的。

只记录经过选择采纳了的原始需求的做法只能保证这些原始需求得到实现,不能保证恰当的、合适的原始需求得到了采纳。而原始需求最关键的地方在于如何寻找最恰当的原始需求,其次才是实现原始需求。

因此,原始需求应当全面的记录来自于各方对产品的要求,无论这个要求是否可行,是否合理,是否要做,是否优先。在记录时,为了不影响后续人员的判断,要尽量传递用户的真实意思,采用用户的语言,保持“原始”。

以产品经理、项目经理为首的产品开发相关人员应当第1时间记录原始需求,积累好原始需求,当要选择做的时候,再来做谨慎的选择。

只有全面的收集记录的原始需求,才能有产品全面的判断和选择,进而开发出受到客户欢迎的产品。

4、冗余 -- 数据一致性是很难保证的

一,为什么要冗余数据

互联网数据量很大的业务场景,往往数据库需要进行水平切分来降低单库数据量。

水平切分会有一个patition key,通过patition key的查询能够直接定位到库,但是非patition key上的查询可能就需要扫描多个库了。

此时常见的架构设计方案,是使用数据冗余这种反范式设计来满足分库后不同维度的查询需求。

例如:订单业务,对用户和商家都有查询需求:

Order(oid, info_detail);

T(buyer_id, seller_id, oid);

如果用buyer_id来分库,seller_id的查询就需要扫描多库。

如果用seller_id来分库,buyer_id的查询就需要扫描多库。

此时可以使用数据冗余来分别满足buyer_id和seller_id上的查询需求:

T1(buyer_id, seller_id, oid)

T2(seller_id, buyer_id, oid)

同一个数据,冗余两份,一份以buyer_id来分库,满足买家的查询需求;一份以seller_id来分库,满足卖家的查询需求。

如何实施数据的冗余,以及如何保证数据的一致性,是今天将要讨论的内容。

二,如何进行数据冗余

(1)服务同步双写

顾名思义,由服务层同步写冗余数据,如上图1-4流程:

业务方调用服务,新增数据

服务先插入T1数据

服务再插入T2数据

服务返回业务方新增数据成功

优点

不复杂,服务层由单次写,变两次写

数据一致性相对较高(因为双写成功才返回)

缺点

请求的处理时间增加(要插入两次,时间加倍)

数据仍可能不一致,例如第二步写入T1完成后服务重启,则数据不会写入T2

如果系统对处理时间比较敏感,引出常用的第二种方案。

(2)服务异步双写

数据的双写并不再由服务来完成,服务层异步发出一个消息,通过消息总线发送给一个专门的数据复制服务来写入冗余数据,如上图1-6流程:

业务方调用服务,新增数据

服务先插入T1数据

服务向消息总线发送一个异步消息(发出即可,不用等返回,通常很快就能完成)

服务返回业务方新增数据成功

消息总线将消息投递给数据同步中心

数据同步中心插入T2数据

优点

请求处理时间短(只插入1次)

缺点

系统的复杂性增加了,多引入了一个组件(消息总线)和一个服务(专用的数据复制服务)

因为返回业务线数据插入成功时,数据还不一定插入到T2中,因此数据有一个不一致时间窗口(这个窗口很短,最终是一致的)

在消息总线丢失消息时,冗余表数据会不一致

不管是服务同步双写,还是服务异步双写,服务都需要关注“冗余数据”带来的复杂性。如果想解除“数据冗余”对系统的耦合,引出常用的第三种方案。

(3)线下异步双写

为了屏蔽“冗余数据”对服务带来的复杂性,数据的双写不再由服务层来完成,而是由线下的一个服务或者任务来完成,如上图1-6流程:

业务方调用服务,新增数据

服务先插入T1数据

服务返回业务方新增数据成功

数据会被写入到数据库的log中

线下服务或者任务读取数据库的log

线下服务或者任务插入T2数据

优点

数据双写与业务完全解耦

请求处理时间短(只插入1次)

缺点

返回业务线数据插入成功时,数据还不一定插入到T2中,因此数据有一个不一致时间窗口(这个窗口很短,最终是一致的)

数据的一致性依赖于线下服务或者任务的可靠性

不管哪种方案,毕竟不是分布式事务,万一出现数据不一致,怎么办呢?

高并发的情况下,实时一致性很难,方法论是:最终一致性

实现方式是:异步检测,异步修复

三,如何保证数据的一致性

(1)线下扫描全量数据法

如上图所示,线下启动一个离线的扫描工具,不停的比对正表T1和反表T2,如果发现数据不一致,就进行补偿修复。

优点

比较简单,开发代价小

线上服务无需修改,修复工具与线上服务解耦

缺点

扫描效率低,会扫描大量的“已经能够保证一致”的数据

由于扫描的数据量大,扫描一轮的时间比较长,即数据如果不一致,不一致的时间窗口比较长

有没有只扫描“可能存在不一致可能性”的数据,而不是每次扫描全部数据,以提高效率的优化方法呢?

(2)线下扫描增量数据法

每次只扫描增量的日志数据,就能够极大提高效率,缩短数据不一致的时间窗口,如上图1-4流程所示:

写入正表T1

第一步成功后,写入日志log1

写入反表T2

第二步成功后,写入日志log2

当然,我们还是需要一个离线的扫描工具,不停的比对日志log1和日志log2,如果发现数据不一致,就进行补偿修复

优点

虽比方法一复杂,但仍然是比较简单的

数据扫描效率高,只扫描增量数据

缺点

线上服务略有修改(代价不高,多写了2条日志)

虽然比方法一更实时,但时效性还是不高,不一致窗口取决于扫描的周期

有没有实时检测一致性并进行修复的方法呢?

(3)线上实时检测“消息对”法

这次不是写日志了,而是向消息总线发送消息,如上图1-4流程所示:

写入正表T1

第一步成功后,发送消息msg1

写入反表T2

第二步成功后,发送消息msg2

这次不是需要一个周期扫描的离线工具了,而是一个实时订阅消息的服务不停的收消息。

假设正常情况下,msg1和msg2的接收时间应该在3s以内,如果检测服务在收到msg1后没有收到msg2,就尝试检测数据的一致性,不一致时进行补偿修复。

优点

效率高

实时性高

缺点

方案比较复杂,上线引入了消息总线这个组件

线下多了一个订阅总线的检测服务

however,技术方案本身就是一个投入产出比的折衷,可以根据业务对一致性的需求程度决定使用哪一种方法。我曾经做过IM系统,好友关系链上亿,好友数据正反表的数据冗余,使用的就是方法二。

四,总结

互联网数据量大的业务场景,常常:

使用水平切分来降低单库数据量

使用数据冗余的反范式设计来满足不同维度的查询需求

冗余数据三种方案:

(1)服务同步双写法能够很容易的实现数据冗余

(2)为了降低时延,可以优化为服务异步双写法

(3)为了屏蔽“冗余数据”对服务带来的复杂性,可以优化为线下异步双写法

保证数据一致性的方案:

(1)最简单的方式,线下脚本扫全量数据比对

(2)提高效率的方式,线下脚本扫增量数据比对

(3)最实时的方式,线上检测“消息对”

进步永远来自于探索,探索是要付出代价的,但是收益更大

原则一:关注于真正的收益而不是技术本身

1、是否可以降低技术门槛加快整个团队的开发流程

系统架构需要能够进行并行开发、并行上线和并行运维,而不会让某个团队成为瓶颈点。

2、是否可以让整个系统可以运行的更稳定。

要让整个系统可以运行的更为的稳定,提升整个系统的 SLA,就需要对有计划和无计划的停机做相应的解决方案。

3、是否可以通过简化和自动化降低成本。

最高优化的成本是人力成本,人的成本除了慢和贵,还有经常不断的 human error。除此之外,是时间成本、资源成本、资金成本。

原则二:以应用服务和 API 为视角,而不是以资源和技术为视角

基础架构和运维工作关注点不同。整个组织和架构的优化,已经不能通过调优单一分工或是单一组件能够有很大提升。一种自顶向下的整体规划、统一设计的方式来大幅提升整体效率;站在服务和对外API的视角来看问题(DevOps),而不是技术和底层的角度。

原则三:选择最主流和成熟的技术

1、尽可能的使用更为成熟更为工业化的技术栈,而不是自己熟悉的技术栈。

2、选择全球流行的技术(普适性),而不是中国流行的技术。

3、尽可能的使用红利大的主流技术,而不要自己发明轮子。

想尽方法跟整个产业、整个技术社区融合和合作,这样才会有最大的收益。

4、绝大多数情况下,如无非常特殊要求,选 Java基本是不会错的。

事务处理、复杂的交易流程 | 社区、Spring框架、中间件、方案

原则四:完备性【扩展性】会比性能更重要

1、使用最科学严谨的技术模型为主,并以不严谨的模型作为补充

2、性能上的东西,总是有很多解的

原则五:制定并遵循服从标准、规范和最佳实践

1、服务间调用的协议标准和规范:Restful API路径、HTTP 方法、状态码、标准头、自定义头,返回数据 json scheme……

2、一些命名的标准和规范:用户 ID,服务名、标签名、状态名、错误码、消息、数据库

3、日志和监控的规范:日志格式,监控数据,采样要求,报警

4、配置上的规范

5、中间件使用的规范。数据库,缓存、消息队列

6、软件和开发库版本统一。软件或开发库的版本最好每年都升一次级,然后在各团队内统一。

Restful API 的规范 | 服务调用链追踪 | 软件升级

原则六:重视架构扩展性和可运维性

扩展性:标准规范且不耦合的业务架构 | 可运维性要求的则是可控的能力

通过服务编排架构来降低服务间的耦合 | 通过服务发现或服务网关来降低服务依赖所带来的运维复杂度

一定要使用各种软件设计的原则:SOLID、IoC/DIP、SOA 或 Spring Cloud

KISS(极简、所见即所得)

原则七:对控制逻辑进行全面收口

1、选择容易进行业务逻辑和控制逻辑分离的技术:JVM+字节码注入+AOP

2、中间件尽量选择AP系统

收口:流量、服务治理、监控、资源调度(CICD)、中间件

原则八:不要迁就老旧系统的技术债务

防腐层 + 规范

原则九:不要依赖自己的经验,要依赖于数据和学习

原则十:千万要小心 X – Y 问题,要追问原始需求

原则十一:激进胜于保守,创新与实用并不冲突

上一篇下一篇

猜你喜欢

热点阅读