《软件架构设计》读书笔记
图片拍摄于2022-03-19 杭州良渚大屋顶
最近看了一本书,书名叫《软件架构设计:大型网站技术架构与业务架构融合之道》,特做一些笔记分享。
这本书整体分为五个部分。
什么是架构
计算机功底
技术架构之道
业务架构之道
从架构得到技术管理
围绕这五个部分总计17章节。
接下来,我会分享部分我感兴趣的章节。
第一部分:什么是架构?
一句话:架构是针对所有重要问题做出的重要决策。
不同公司或者相同公司在不同的阶段所面临的问题不同,架构自然也会有所不同。
个人认为,不存在称之为完美的架构,只会存在最适合的。面对的场景,着重的目的不同,那么相应的决策也会不同(有点废话)。
架构的分类。
作者从技术的角度,把软件从底向上分层,做了架构的分类。
第一层:基础架构
基础架构指的是云平台、操作系统、网络、存储、数据库和编译等。
第二层:中间件和大数据平台
中间件,例如分布式服务中间件、消息中间件、数据库中间件、缓存中间件等。
第三层:业务系统架构
通用软件系统。例如常用办公软件、播放器。
离线业务。比如各种基于数据的离线计算、数据挖掘。
大型在线系统。比如电商、广告、搜索、推荐、ERP或者CRM等。
整体就像这样,
从上面你也可以看出,只有大厂这三层都有。像小公司可能只有第三层,或者小量的第二层。 印象里,我前司是没有第一层的,第二层是有的。
一般情况下,每一层都会有专门的人去干活。比如第二层会有专门的中间件部门, 对应又分为几个组,每个组负责对应的中间件开发。
业务部门在第三层,一般情况下,他们只负责业务的curd,如果有场景需要用到一些中间件时, 这时候通常会去找负责中间件的人对接,使用他们的sdk等。(ps:好不好用那就是另外一回事了)
还有一个有意思点,作者在书中提到架构的道与术。
什么是架构的道?
抽象点说,对于技术问题,主要是指高并发、高可用和一致性方面。对于业务问题,主要指业务需求分析和建模。
那么,我们在面对这些问题的时候, 是通过大量的业务系统实践,在实践基础上进行的思考和总结,进而提炼出的一些方法论,这就是道。
更具体的说,比如,
数据库如何分库分表?
分库分表的时机如何确定?
缓存一致性问题如何解决?
如何拆分服务?
……
等等问题,这些问题解决方案并不是凭空出现的,而是通过大量的实践落地进而总结产生的一套解决方案核心思路。
所以道很多时候是”虚”的东西,越虚意味着就越抽象,如果两个人在讨论某个问题,而对一些专业理论的认知还未处于同一水平上,那听起来就只能离谱了。
所以要讲道之前,得先有术。术就是指对应具体的语言,框架或者中间件使用姿势。这些都是比较具体的东西,实操性强,方便大家理解。
架构的道和术,都不能偏废,一方面需要不断实践(术),在实践中深入原理。进而把实践的东西抽象,总结出来,形成方法论(道)。 不断地用道来指导新的术,在新的术中再总结出新的道,如此循环往复。
以上是第一部分内容。
第二部分:计算机功底
主要讲解的是术。计算机功底、语言、框架、网络、数据库、操作系统等。
印象最深刻的是框架那一章。作者提到,熟悉一个框架之后,更多的是应该去关注它的缺点,而不是优点。更应该关注它不能做什么,而不是它能做什么。 它不能做什么往往是别的框架的改进点。
细想,如果你不关注它不能做什么,在你们拍板决定使用框架时,做了一半发现, 核心的一块需求它支持不了,这时候只能欲哭无泪了。
第三部分:技术架构之道
主要讲解的是道。 里面分为:
高并发问题
高可用与稳定性
事务一致性
多副本一致性
CAP理论
因为这一部分主要是关于道方面的,所以很多地方是抽象化的。读者在读这一部分时候,针对一些问题的解决方案,需要自行去思考部分细节。
我主要分享高并发和事务一致性的笔记。其他读者可以自行查看。
高并发问题
要让各种各样的功能和逻辑在计算机系统中实现,只能通过读和写两个操作。
基于这个基础,在高并发问题上,书中对问题进行了分类。
侧重”高并发读”的系统
比如,搜索引擎、电商的商品搜索、微博热点、知乎热点等。
侧重”高并发写”的系统
比如,广告系统计费等
两者兼顾。
比如,电商的库存系统、秒杀系统、IM、朋友圈等。
之所以这样区分,是因为处理的策略会不同。
如果是侧重高并发读的,一般可以采取以下策略。
策略1:加缓存
案例一:本地缓存或Memcached/Redis
对应的加缓存需要考虑以下问题,
单点问题
缓存雪崩
缓存穿透
缓存击穿
单点问题,那么就需要多节点集群部署,通过类似心跳、哨兵模型等能自动剔除挂掉的节点机制。
后面三个问题一般和回源策略有关。
一种是不回源,只查询缓存。缓存没有,响应空。这种一般需要主动更新缓存,并且不设置过期时间,不会存在缓存击穿,大量key过期问题。
一种是缓存失效,需要回源。就需要考虑上面的问题。 对应缓存雪崩的问题,一般我们会在缓存本身时间的基础上,加上随机值,不让大批key同一时间失效。 对于缓存穿透的问题,一般上游做参数的校验,防止恶意请求。
对于符合规则的key,如果数据库没有, 一般会直接把这个key缓存起来,设置一个短暂的时间,值对应设置空。当然还可以通过布隆过滤器来实现,这时候,数据量已经很大了。
对应缓存击穿问题,一般我们采取的方式是保证同一时刻只有第一个请求打到db层,剩下的等待第一个同步结果即可。
案例二:Mysql的Master/Slave
上面提到的是简单的<key,val>缓存,还有一些场景会涉及到多表关联查询(如果避不开的话),如果直接查业务库,高并发访问肯定顶不住的。 一般情况下,往往是通过主从部署,业务直接查从库关联就行了。分摊主库的压力(前提是没有进行分库)。
当前这种情况,高并发下还是会有问题,从库的压力还是太大了。这时候也可以把查询的结果缓存起来,下次直接从缓存拿。不过需要注意的是, 当关联的数据发生变化的时候,缓存就得更新了。
案例三:CDN静态文件加速
这个不用再描述了吧。
策略2:并发读
案例1:并发调用
核心思想就是,如果一个请求需要调用不同的三个服务a,b,c,且这三个服务相互之间不存在依赖关系,也就是请求c不依赖a或者b的结果, 那么请求就可以并发调用。总时间从(a+b+c)到max(a,b,c)。
案例2:Google 的”冗余请求”
假设一个用户的请求需要100台服务器同时联合处理,每台服务器有1%的概率发生调用延迟(假设定义响应时间大于1s为延迟), 那么响应时间大于1s的概率是63%(计算规则可以自行查看) 咋么解决这个问题? 冗余请求。简单点就是客户端同时向多台服务器发送请求,哪个返回的快就用哪个,其他丢弃。
策略3:重写轻读
重写轻读的核心思路就是,
为什么需要这样? 因为有时候一个数据源往往不能满足复杂的业务查询,或者说无法满足业务查询多样性的问题。那么我们就要多写,专业术语叫写扩散。
首先我们保证主业务写入成功,然后通过消息队列这样的异步手段,写入不同需求维度的库。不同维度的业务只需要去对应的数据源查询即可。
对于分库分表后的关联查询,需要从多个库查询数据再聚合,但是无法利用数据原始的join功能,只能在内存中做数据的聚合。
这时候存在一个问题,无法使用数据库本身的分页功能。 一般情况下,解决方案还是重写轻读的策略,提前把数据关联好数据,存在一个地方。业务直接读取聚合好的数据。
也可以利用es类的搜索引擎来实现。把多个表的join结果做成一个个文档,放在搜索引擎里面。
如果是侧重高并发写,自己看去吧。
数据一致性问题
数据一致性问题无处不在。这么说吧,只要一个场景存在需要操作两个或者两个以上的服务,那么就必然存在一致性问题。 比如,A服务和B服务。无论是先操作A,再操作B,还是先操作B,再操作A,都存在第一步成功,第二步失败的可能性。
又比如,数据库的Master-Salve异步复制,如果Master宕机,还有部分数据未来得及同步给Salve,此时切换到Salve,会导致部分数据丢失。
这样的一致性问题在分布式服务中无处不在。
一个订单服务,一个库存服务,下订单的时候无论是先扣库存,后创建订单,还是先创建订单,再扣库存,都存在第二步失败的可能,如何保证服务之间的 数据一致性问题?
分布式事务解决方案
数据库层面的2PC
2PC有两个角色:事务参与者和事务协调者。具体到数据库层面,每一个数据库就是一个参与者,调用方就是协调者。 同时2PC分为两个阶段。
准备阶段一
提交阶段二
准备阶段。协调者向每个服务发起询问,执行一个事务,每个参与者需要回复yes、no或者调用超时。
提交阶段。如果所有参与者回复yes,那么事务协调者向所有参与者发送事务commit操作。所有参与者执行各自事务,然后发送ACK。 如果有一个事务参与者回复No或者超时,那么事务协调者向所有参与者发送rollback操作,所有参与者各自回滚事务,然后回复ACK。
当然2PC也存在问题。
问题1:性能问题。在阶段一锁定资源后,要等所有的参与者回复完,然后才能一起进入阶段二,不能很好应对高并发场景。
问题2:阶段一完成后,如果此时协调者宕机,所有参与者收不到阶段二commit或者rollback请求,状态处在尴尬的位置。
问题3:阶段一完成,阶段二中,事务协调者向所有参与者发送了指令,但是有一个参与者超时或者没有正确返回ACK。这时候,其他参与者应该咋么办?
消息中间件(最终一致性)
简单地说,系统A收到用户扣钱请求,系统A先自己扣钱,也就是操作自己的数据库DB1。 然后通过消息中间件给系统B发送一条加钱的消息。系统B收到这条消息,给自己账户加钱,也就是操作数据库DB2。
问题来了,操作数据库A和投递消息到消息中间件是两个动作,两个网络请求。无论你是先投递消息,再更新数据库,还是先更新数据库再投递消息, 都存在第二步可能失败的场景。
如果第一步操作数据库成功, 投递消息失败,重试还是失败咋么办?
如果第一步操作数据库成功,投递消息失败,重试中还是失败,而且刚好pod又重启了,现场都没了咋么办?
如果第一次操作数据库成功,投递消息失败(只是中间件响应的时候失败,其实消息是投递出去了),消息重发了咋么办?
业务方自己实现
我们担心无非是两个问题。
第二步投递失败,重试次数上限现场丢失或者pod重启现场丢失。
消息重复被消费
我们可以这样,系统A新增一个消息表(落盘的意思),系统A不再直接给消息中间件发送消息,而是把要发送的数据写入消息表,把扣钱和写消息表的动作放在同一个动作里,保证两者原子性。
系统A开启一个后台程序,把消息表的消息投递给消息中间件,如果失败,不断尝试,除非消息中间件明确响应ACK。可以确保消息最终一定会发送成功。
对于消费者系统B来说,从消息中间件获取消息,除非明确响应ACK,否则的话中间件会认为这条消息未被消费,会导致重复消费的问题。一般情况下,通过业务的唯一值去重即可。
这个方案的缺点是,业务方要新增一个消息表,同时需要一个后台任务。
基于事务消息的中间件
比如RocketMQ会把消息的发送分成两个阶段:Prepare阶段(消息预发送)和Confirm(确认发送)。
步骤一:系统A调用Prepare接口,预发送消息。此时消息保存在消息中间件中,不会发送给消费者。
步骤二:系统A更新数据库,进行扣钱操作。
步骤三:系统A调用Confirm接口,确认操作,确认之后消息中间件才会把消息投递给消费者消费。
这就会涉及到,
步骤一成功,步骤二成功,步骤三调用失败,咋么办?
步骤一成功,步骤二失败,步骤三不会执行,咋么办?
RocketMQ关键点在于,它会定期去扫描那些所有预发送但是还没确认的消息,回调给发送方,询问消息是发送还是取消。发送方根据具体的情况决定。 这个方案和上面自己实现的最大特点在于,扫描消息表的工作被中间件接替了。
但是对应消息表,业务还是需要存储的,否则消息回调确认的时候,业务不知晓消息具体该如何操作?
其他例如 TCC、Saga、对账等方案可以自行查看。
第四部分:业务架构之道
书中业务架构之道有一段对优秀的分层架构的描述还是非常认可的。
越底层的系统越单一、越简单、越固化;越上层的系统花样越多、越容易变化,要做到这一点,需要层与层之间有很好的隔离和判断。
关于边界思维,书中提到,
架构强调的不是系统能支持什么,而是系统的”约束”是什么,不管是业务架构,还是技术架构,没有”约束”,就没有架构。 一个设计或者系统,如果无所不能,则意味着一无所能。
在”为何很难设计一个好的领域模型” 讨论中,我们普遍意识中的几个因素:
现实业务的复杂性,职责的分化,短期内很少深入理解业务。
业务迭代速度太快。
书中提到一个可能很多人都没想过的问题:意识问题,也可以称为思维问题。
在用户、产品人员、运营人员眼中,沟通的语言是流程而不是模型。开发人员在和他们沟通中,慢慢形成以流程为主导,而不是以模型为主导的思维方式。这就导致开发过程是流程驱动的,而不是领域驱动。大家在讨论业务和系统解决方案的时候,大多时间都花在了业务流程、业务规则上,而不是挖掘流程背后的不变因素。
我突然意识到,这个也是决定普通程序员和高级程序员的因素之一。
分享完毕。
本作品采用《CC 协议》,转载必须注明作者和本文链接