你好,我是编辑王惠,今天初四啦,同学们过年好啊~
今天呢,我们继续来复盘课程的第二个模块“架构师的视角”中的核心知识点,以及再次来感受、学习下在该模块中各位优秀同学的所学所得、所思所想。
在这个模块里,我们系统性地了解了在做架构设计时,架构师都应该思考哪些问题、可以选择哪些主流的解决方案和行业标准做法,以及这些主流方案都有什么优缺点、会给架构设计带来什么影响,等等,以此对架构设计这种抽象的工作有了更具体、更具象的认知。
让计算机能够跟调用本地方法一样,去调用远程方法,应该是RPC的终极目标。但是目前的技术水平无法实现这一终极目标,所以就有了其他更可行的折中方案。
事物是慢慢演化发展的,目标可以远大,但是做事还是要根据实际情况,实事求是。
RPC只是服务(进程)之间简化调用的一种方式,它可以让开发者聚焦于业务本身。而对于服务间通信的各种细节交给框架处理这个维度来说,如果撇开这一层面,分布式系统的服务调用可以采用任何一种通信方式,比如HTTP、Socket等。
对于REST,我的第一印象就是服务端无状态,有利于水平扩展,但更多的是停留于具体的技术层面。
过程如果有意义的话,一定会产生一个结果,这个结果就是资源的状态发生了转移(幂等前提下的重试不算),但是过程的细节更多,所以抽象程度无法做到向面向资源看齐。在一个过程中,我们可以对有关联关系的不同层次与结构的资源同时进行处理,但是面向资源却不容易做到。
我能想到的例子是转账操作,同时操作两个资源(即账户),而且要保证事务的ACID。在转账的过程中要处理很多异常情况,尤其是涉及到多方交易的时候,所以写这样的交易就非常复杂,容易出错。
如果用面向资源的角度去考察,可以看成是对三个资源的操作:转出账户、转入账户以及事务。这里把事务列为单独的资源,是为了呼应上面提到的一个资源状态变化引起的关联资源的变化。
如果转账操作利用TCC(Try-Confirm-Cancel)的方法,我觉得就是一种更偏向于面向资源的做法,每次只改变一个资源的状态。如果某个关联资源的状态改变失败,就对它发起一个逆操作(比如冲正)。这样可以做到很高的并发,在做到保序性的前提下,做差错处理也很简单。相当于把一个复杂操作分解成了多个简单操作,这样开发起来也很快,很容易复用。有点类似CISC和RISC指令集的关系。
谢谢老师的分享,我也谈谈自己实践REST和RPC后的感想,主要在第一、第二的争议点,感触非常深。
争议一:面向资源的编程思想只适合做CRUD,只有面向过程、面向对象编程才能处理真正复杂的业务逻辑。- 争议二:REST与HTTP完全绑定,不适用于要求高性能传输的场景中。
之前我们用REST到了第2成熟度,关于第一点争议,包括我现在的想法,也是认为面向资源更加适合做数据读写接口的场景,例如某NoSQL的应用服务API封装,或者提供某内部使用API的微服务更加适合。
原因主要有两个。首先,如果是作为提供给前端使用,处理起复杂业务的时候不好抽象;其次,原本一个接口可以处理的复杂逻辑,但是因为REST原因,导致接口粒度要细到N个,假如由前端人员对接,那么就会增大他们的业务组合难度(我更加倾向大部分的业务逻辑由后端解决,前端尽量关注数据展示与动画交互,数据离后端人员最近)。
那么当粒度细化了以后,就会引申出第二个争议所说的性能问题。这里也有两方面原因:首先是因为要做接口的编排组合;其次也是因为REST被HTTP绑得死死的,那么开发人员就不得不去关注那些细节了。
举个例子,HTTP Status Code的参数,除了是body外可能还会是header,也有可能是URI参数,对于实际开发的便捷度来说并不够友好。
另外课程中老师还提到了GraphQL。该技术的确能缓解Query的部分问题,但是我认为它同时也存在不可避免的问题,就是如何让使用端可以很好地了解并对接数据源及其属性?
对于这种存在争议性的东西,我的建议是尽可能地少引入团队。毕竟争议性越大,就意味着大家对它的理解越少,无论是引入推广还是实施的具体效果,都会存在很长周期的磨合与统一。
不过它也不是一文不值的。我们团队做的是解决方案,解决的是针对性的问题场景,对于一些比较清晰、比较接近数据的场景,如NoSQL的API,或者简单的、方便抽象的、相对需求稳定的业务场景,如某内部微服务的API,我认为是可以尝试使用的。
FORCE策略要求事务提交后,变动的数据马上写入磁盘,没有日志保护,但是这样不能保证事务的原子性。比如用户的账号扣了钱、写入了数据库,这时候系统崩溃了,商品库存的变更信息和商家账号的变更信息都还没来得及写入数据库,这样数据就不一致了。
因此为了实现原子性,保证能够恢复崩溃,绝大多数的数据库都采用NO-FORCE策略。而为了实现NO-FORCE策略,就需要引入Redo Log(重做日志)来实现,即使修改数据时系统崩溃了,重启后根据Redo Log,就可以选择恢复现场,继续修改数据,或者直接回滚整个事务。
换句话说就是,我先把我要改的东西记录在日志里,再根据日志统一写到磁盘中。万一我在写入磁盘的过程中晕倒了,等我醒来的时候,我照着日志重新做一遍,也能成功。
Commit Logging方式实行了NO-FORCE策略,照理说这样已经实现了事务的功能,已经很牛了,但是当一个事务中的数据量特别大的时候,等全部变更写入Redo Log然后再统一写入磁盘,这样性能就不是很好,就会很慢,老板就会不开心。
那能不能在事务提交之前,偷偷地先写一点数据到磁盘呢(偷跑)?
答案是可以的,这就是STEAL策略。但是问题来了,你偷摸地写了数据,万一事务要回滚,或者系统崩溃了,这些提前写入的数据就变成了脏数据,必须想办法把它恢复才行。
这就需要引入Undo Log(回滚日志),在偷摸写入数据之前,必须先在Undo Log中记录都写入了什么数据、改了什么地方,到时候事务回滚了,就按照Undo Log日志,一条条恢复到原来的样子,就像没有改过一样。
这种能够偷摸先写数据的方式,就叫做Write-Ahead Logging。性能提高了,同时也更复杂了。不过虽然它复杂了点,但是效果很好啊,MySQL、SQLite、PostgreSQL、SQL Server等数据库都实现了WAL机制呢。
在软件开发的发展历程中,“提供简洁的API”始终贯穿至今,因此这一讲中提到的透明事务,在我看来对普通开发人员的使用层面来说,是完全有必要的。
但是作为开发人员,一定要有精益求精的品质,也许我们在日常使用中已经习惯了使用简洁的API来实现强大的功能。但如果遇到棘手的问题,或者需要自己思考解决方案的场景,那么“内功”就能显露出它的威力。
学校组织知识竞赛,学生们(参与者)以一组为单位参加比赛,由一个监考老师(协调者)负责监考。考试分为考卷和答题卡,学生必须先在十分钟内把答案写在考卷上(记录日志),然后在三分钟内把答案涂到答题卡上(提交事务)。
两段式提交
三段式提交
XA事务成立的前提是所有的服务都可用,在分布式环境下使用的代价是如果有一个服务不可用,那么整个系统就不可用了。另外,XA事务可能会对业务有侵入,而依靠可靠消息队列和重试机制则不需要侵入业务。
我觉得可靠事件队列最适用的场景就是在内部系统中做高可靠。
TCC的范围扩大了一些,适合于新设计的系统;SAGA的适用性最广,因为对服务提供的接口没有要求,可以有落地人工处理做保证。我们现在涉及三方互联的老系统,都可以看作是SAGA的一种形式。
一个不起眼的DNS竟然暗藏了这么多精妙的设计,计算机技术发展的每个阶段成果,都是人类智慧的结晶。
关于奥卡姆剃刀原则,我也想做点补充。奥卡姆剃刀原则,又被称为“简约之原则”,它是由14世纪圣方济各会修道士奥卡姆(英格兰的一个地方)的威廉(William of Occam)提出来的,他说过这样一段话:
切勿浪费较多东西,去做“用较少的东西,同样可以做好的事情”。
更有名的一句话是:如无必要,勿增实体。
在历史上各个时代,最高深的物理学理论,从形式上讲都不复杂,从牛顿力学,到爱因斯坦的相对论,到今天物理学的标准模型。例如,质能方程 E=mc^2 ,欧拉恒等式 e^(iπ) + 1 = 0,都以极简的方式描述了极其复杂的规律。
关于计算机系统,在能满足需求的前提下,最简单的系统就是最好的系统。很多人为了显示自己的技术水平,明明是很简单的需求,却上了一堆高大上的技术,为了技术而技术,忘了技术的本质是为业务服务的,这显然违背了奥卡姆剃刀原则。
我在做前端开发的时候,遇到了微信会自动缓存页面静态资源的问题,必须要手动刷新页面才行,有时候还得刷新好几遍才可以,有些极端情况则是短时间内连续刷新依然显示旧页面,这个问题在公司内一些同事的手机上均出现过。学习了周老师的这节课,对缓存有了基本的了解,明天就用Charles抓包,看看微信内对网页的客户端缓存策略是什么。
这里想补充一下QUIC相对于TCP的两点内容:
其实应用层的协议多种多样,比如直播的RTMP、物联网终端的MQTT等,但感觉都是两害取其轻的专项优化、对症下药的方案。只有QUIC直面了TCP的问题,通过应用层的编码实现,系统地提供更好的“TCP连接”。
在网上看到华为云CDN主要的应用场景,希望借此可以帮助我们更好地理解内容:
为什么负载均衡不能只在某一个网络层次中完成,而是要进行多级混合的负载均衡?
因为每一个网络层的功能是不一样的,这样就决定了每一层都有自己独有的数据,在不同的网络层做负载均衡能达到不同的效果。比如要修改MAC地址,在数据链路层修改最方便,要修改IP地址最好在网络层修改。
关于网络分层,我这里也打个比方。小帅在网上下单买东西,卖家需要寄快递,把要寄的商品(物理层)打包到包装盒里(数据链路层),然后把包装盒放到快递盒子里(网络层),在快递单上写上寄件地址和收件地址(Headers)。
然后快递员打电话给小帅拿快递(传输层),这里出现了TCP三次握手连接:
小帅拿到快递后,在网上点击确认收货按钮,确认收货(返回 http Status Code 200,应用层)。
“能满足需求的前提下,最简单的系统就是最好的系统”。这句话的隐藏前提是,我们的选择空间要足够大。不管是CDN、负载均衡、客户端缓存、服务端缓存,还是分布式缓存,都给我们提供了大量的选择余地,可以根据自己系统的实际情况,灵活地选择最适合的方案。
这句话的另一种理解是:没有最好的方案,只有最合适的方案。
角色是为了解耦用户和权限之间的多对多关系。比如有100个用户,他们的权限都是一样的,如果给每个用户都设一遍权限,这就太麻烦了,而且还很容易出错。这时候设置一个角色,把对应的权限配置到角色上,然后这100个用户加到这个角色中就行了。
角色还有一个好处,如果角色的权限变了,所有角色中用户的权限也会同时变更,不用一个个用户去设置了。
许可是为了解耦操作与资源之间的多对多关系,比如有新增用户、编辑用户、删除用户的三种操作,通常这些都是一起的,要么都能操作,要么都不能操作。这时候就可以把这三种操作打包成一个用户维护许可,用许可和角色关联更简洁。
关于Spring Security中的Role和Authority我是这么理解的:Role就是普通的角色,拥有一组许可,这个角色下的所有用户的权限都是一样的。
但是如果一个角色中的一些用户有个性化的需求,比如销售助理角色,本来没有查看客户的权限,但是某个销售助理比较特殊,需要查看客户的信息,这时如果是单角色的系统,就需要新增一个“销售助理可查看客户角色”,这样很容易导致角色数量爆炸。
而有了Authority,就可以满足这种个性化需求,只要把查看客户的权限加到Authority中赋予用户就行了。
Cookie-Session就相当于是坐飞机托运了行李,只要带着登机牌就行了。但是一旦托运了行李,行李就和飞机绑定了,你就不能随意换航班了。
JWT就相当于是坐飞机拎着行李到处跑,每次过安检还要打开行李箱检查,而且箱子太小也带不了多少东西。但它的优点是可以随意换航班,行李都在自己身边。