春节特别放送(下)_ 积累沉淀,知行合一 你好,我是编辑王惠,今天初四啦,同学们过年好啊~

今天呢,我们继续来复盘课程的第二个模块“架构师的视角”中的核心知识点,以及再次来感受、学习下在该模块中各位优秀同学的所学所得、所思所想。

“架构师的视角”模块内容复盘

在这个模块里,我们系统性地了解了在做架构设计时,架构师都应该思考哪些问题、可以选择哪些主流的解决方案和行业标准做法,以及这些主流方案都有什么优缺点、会给架构设计带来什么影响,等等,以此对架构设计这种抽象的工作有了更具体、更具象的认知。

服务风格设计

  • 远程服务调用: RPC以模拟进程间方法调用为起点,表示数据、传递数据和表示方法,是RPC必须解决的三大基本问题。解决这些问题可以有很多方案,这也是 RPC 协议/框架出现群雄混战局面的一个原因,而另一个原因是简单的框架很难能达到功能强大的要求。一个RPC框架要想取得成功,就要选择一个发展方向,因此我们也就有了朝着面向对象发展、朝着性能发展和朝着简化发展这三条线。
  • RESTful服务: 面向过程和面向对象两种编程思想虽然出现的时间有先后,但在人类使用计算机语言来处理数据的工作中,无论用哪种思维来抽象问题都是合乎逻辑的。而面向资源编程这种思想,是把问题空间中的数据对象作为抽象的主体,把解决问题时从输入数据到输出结果的处理过程,看作是一个(组)数据资源的状态不断发生变换而导致的结果。这符合目前网络主流的交互方式,所以REST常常被看作是为基于网络的分布式系统量身定做的交互方式。

事务处理

  • 本地事务: 本地事务是指仅操作特定单一事务资源的、不需要“全局事务管理器”进行协调的事务。ARIES理论提出了Write-Ahead Logging式的日志写入方法,通过分析、重做、回滚三个阶段实现了STEAL、NO-FORCE,从而实现了既高效又严谨的日志记录与故障恢复。此外在实现隔离性这方面,我们要知道不同隔离级别以及幻读、脏读等问题,都只是表面现象,它们是各种锁在不同加锁时间上组合应用所产生的结果,锁才是根本的原因。
  • 全局事务: 全局事务可以理解为是一种适用于单个服务使用多个数据源场景的事务解决方案,其中的两段式提交和三段式提交模式还会在一些多数据源的场景中用到,它们追求ACID的强一致性,这个目标不仅给它带来了很高的复杂度,而且吞吐量和使用效果上也不够好。
  • 共享事务: 共享事务是指多个服务共用同一个数据源,虽然目前共享事务确实已经很少见,不过通过了解事务演进的过程,也更便于我们理解其他三种事务类型。
  • 分布式事务: 现在系统设计的主流,已经变成了不追求ACID而是强调BASE的弱一致性事务,也就是分布式事务,它是指多个服务同时访问多个数据源的事务处理机制。我们要知道,分布式系统中不存在放之四海皆准的万能事务解决方案,针对具体场景,选择合适的解决方案,达到一致性与可用性之间的最佳平衡,是我们作为一名设计者必须具备的技能。

透明多级分流系统

  • 客户端缓存: 客户端缓存具体包括“状态缓存”、“强制缓存”和“协商缓存”三类,利用好客户端的缓存能够节省大量网络流量,这是为后端系统分流,以实现更高并发的第一步。
  • 域名解析: 域名解析对于大多数信息系统,尤其是基于互联网的系统来说是必不可少的组件,它的主要作用就是把便于人类理解的域名地址,转换为便于计算机处理的IP地址。
  • 传输链路: 这也是一种与客户端关系较为密切的传输优化机制。这里我们要明确一点,即HTTP并不是只将内容顺利传输到客户端就算完成任务了,如何做到高效、无状态也是很重要的目标。另外在HTTP/2之前,要想在应用一侧优化传输,就必须要同时在其他方面付出相应的成本,而HTTP/2 中的多路复用、头压缩等改进项,就从根本上给出了传输优化的解决方案。
  • 内容分发网络: 内容分发网络(CDN)是一种已经存在了很长时间,也被人们广泛应用的分流系统,其工作过程主要涉及到路由解析、内容分发、负载均衡和它所能支持的应用内容四个方面。CDN能为互联网系统提供性能上的加速,也能帮助增强许多功能,比如说安全防御、资源修改、功能注入等。而且这一切又实现得极为透明,可以完全不需要开发者来配合。
  • 负载均衡: 负载均衡的两大职责就是“选择谁来处理用户请求”和“将用户请求转发过去”。如今一般实际用于生产的系统几乎都离不开集群部署,而在其中用于承担调度后方的多台机器,以统一的接口对外提供服务的技术组件,就是负载均衡器了。理解其工作原理,对于我们做系统的流量和容量规划工作是很有必要的。
  • 服务端缓存: 服务端缓存也是一种通用的技术组件,它主要用于减少多个客户端相同的资源请求,缓解或降低服务器的负载压力,因此可以作为一种分流手段。

安全架构

  • 认证: 认证解决的是“你是谁?”的问题,即如何正确分辨出操作用户的真实身份。在课程中我们了解了三种主流的认证方式,分别为通讯信道上的认证、通讯协议上的认证、通讯内容上的认证。
  • 授权: 授权解决的是“你能干什么?”的问题,即如何控制一个用户该看到哪些数据、能操作哪些功能。我们可以使用OAuth 2.0来解决涉及到多方系统调用时可靠授权的问题,而针对如何确保授权的结果可控的问题,可以通过基于角色的访问控制(RBAC)来解决。
  • 凭证: 凭证解决的是“你要如何证明?”的问题,即如何保证它与用户之间的承诺是准确、完整且不可抵赖的。对此我们也了解了Cookie-Session机制和无状态的JWT两种凭证实现方案,它们分别适用于不同的场景,因此我们在做架构设计时要做好权衡。
  • 保密: 即解决如何保证敏感数据无法被内外部人员所窃取、滥用的问题。这里我们要知道,保密是有成本的,追求越高的安全等级,我们就要付出越多的工作量与算力消耗。
  • 传输: 即解决如何保证通过网络传输的信息无法被第三方窃听、篡改和冒充的问题。传输环节是最复杂、最有效,但又是最早就有了标准解决方案的,不管是哈希摘要、对称加密和非对称加密这三种安全架构中常见的保密操作,还是通过数字证书达成共同信任、通过传输安全层隐藏繁琐的安全过程。
  • 验证: 验证解决的是“你做的对不对?”的问题,即如何确保提交的信息不会对系统稳定性、数据一致性、正确性产生风险。虽然貌似数据验证并不属于安全的范畴,但其实它与程序如何编码是密切相关的。这里我们需要明确一点,就是缺失的校验会影响数据质量,而过度的校验也不会让系统更加健壮,反而在某种意义上会制造垃圾代码,甚至还会有副作用。

模块留言精选

第7讲

来自@zhanyd

让计算机能够跟调用本地方法一样,去调用远程方法,应该是RPC的终极目标。但是目前的技术水平无法实现这一终极目标,所以就有了其他更可行的折中方案。

事物是慢慢演化发展的,目标可以远大,但是做事还是要根据实际情况,实事求是。

第8讲

来自@Mr.Chen

RPC只是服务(进程)之间简化调用的一种方式,它可以让开发者聚焦于业务本身。而对于服务间通信的各种细节交给框架处理这个维度来说,如果撇开这一层面,分布式系统的服务调用可以采用任何一种通信方式,比如HTTP、Socket等。

第9讲

来自@tt

对于REST,我的第一印象就是服务端无状态,有利于水平扩展,但更多的是停留于具体的技术层面。

过程如果有意义的话,一定会产生一个结果,这个结果就是资源的状态发生了转移(幂等前提下的重试不算),但是过程的细节更多,所以抽象程度无法做到向面向资源看齐。在一个过程中,我们可以对有关联关系的不同层次与结构的资源同时进行处理,但是面向资源却不容易做到。

我能想到的例子是转账操作,同时操作两个资源(即账户),而且要保证事务的ACID。在转账的过程中要处理很多异常情况,尤其是涉及到多方交易的时候,所以写这样的交易就非常复杂,容易出错。

如果用面向资源的角度去考察,可以看成是对三个资源的操作:转出账户、转入账户以及事务。这里把事务列为单独的资源,是为了呼应上面提到的一个资源状态变化引起的关联资源的变化。

如果转账操作利用TCC(Try-Confirm-Cancel)的方法,我觉得就是一种更偏向于面向资源的做法,每次只改变一个资源的状态。如果某个关联资源的状态改变失败,就对它发起一个逆操作(比如冲正)。这样可以做到很高的并发,在做到保序性的前提下,做差错处理也很简单。相当于把一个复杂操作分解成了多个简单操作,这样开发起来也很快,很容易复用。有点类似CISC和RISC指令集的关系。

第10讲

来自@陈珙

谢谢老师的分享,我也谈谈自己实践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,我认为是可以尝试使用的。

第11讲

来自@zhanyd

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机制呢。

第12讲

来自@Wacky小恺

在软件开发的发展历程中,“提供简洁的API”始终贯穿至今,因此这一讲中提到的透明事务,在我看来对普通开发人员的使用层面来说,是完全有必要的。

但是作为开发人员,一定要有精益求精的品质,也许我们在日常使用中已经习惯了使用简洁的API来实现强大的功能。但如果遇到棘手的问题,或者需要自己思考解决方案的场景,那么“内功”就能显露出它的威力。

第13讲

来自@zhanyd

学校组织知识竞赛,学生们(参与者)以一组为单位参加比赛,由一个监考老师(协调者)负责监考。考试分为考卷和答题卡,学生必须先在十分钟内把答案写在考卷上(记录日志),然后在三分钟内把答案涂到答题卡上(提交事务)。

两段式提交

  • 准备阶段: 老师宣布:“开始填写考卷,时间十分钟”。十分钟内,写好考卷的学生就回答:Prepared。十分钟一到,动作慢还没写好的学生,就回答:Non-Prepared。如果有学生回答Non-Prepared,该小组被淘汰。
  • 提交阶段: 如果所有的学生都回答了Prepared,老师就会在笔记本上记下,“开始填答题卡”(Commit),然后对所有的学生说:“开始填答题卡”(发送 Commit 指令)。学生听到指令后,就开始根据考卷去涂答题卡。
  • 如果学生在涂答题卡的时候,过于紧张把答题卡涂错了,还可以根据考卷重新涂;如果所有的学生在规定时间内都填好了答题卡,老师宣布该小组考试通过。

三段式提交

  • CanCommit阶段: 老师先给学生看一下考卷,问问学生能不能在十分钟内做完。如果有学生说没信心做完,该小组直接淘汰。
  • PreCommit阶段: 如果学生都说能做完,老师就宣布:“开始填写考卷,时间十分钟”,和两段式提交的准备阶段一样。
  • DoCommit阶段: 和两段式提交的提交阶段一样。

第14讲

来自@Goku

XA事务成立的前提是所有的服务都可用,在分布式环境下使用的代价是如果有一个服务不可用,那么整个系统就不可用了。另外,XA事务可能会对业务有侵入,而依靠可靠消息队列和重试机制则不需要侵入业务。

第15讲

来自@tt

我觉得可靠事件队列最适用的场景就是在内部系统中做高可靠。

TCC的范围扩大了一些,适合于新设计的系统;SAGA的适用性最广,因为对服务提供的接口没有要求,可以有落地人工处理做保证。我们现在涉及三方互联的老系统,都可以看作是SAGA的一种形式。

第16讲

来自@zhanyd

一个不起眼的DNS竟然暗藏了这么多精妙的设计,计算机技术发展的每个阶段成果,都是人类智慧的结晶。

关于奥卡姆剃刀原则,我也想做点补充。奥卡姆剃刀原则,又被称为“简约之原则”,它是由14世纪圣方济各会修道士奥卡姆(英格兰的一个地方)的威廉(William of Occam)提出来的,他说过这样一段话: 切勿浪费较多东西,去做“用较少的东西,同样可以做好的事情”。

更有名的一句话是:如无必要,勿增实体。

在历史上各个时代,最高深的物理学理论,从形式上讲都不复杂,从牛顿力学,到爱因斯坦的相对论,到今天物理学的标准模型。例如,质能方程 E=mc^2 ,欧拉恒等式 e^(iπ) + 1 = 0,都以极简的方式描述了极其复杂的规律。

关于计算机系统,在能满足需求的前提下,最简单的系统就是最好的系统。很多人为了显示自己的技术水平,明明是很简单的需求,却上了一堆高大上的技术,为了技术而技术,忘了技术的本质是为业务服务的,这显然违背了奥卡姆剃刀原则。

第17讲

来自@追忆似水年华

我在做前端开发的时候,遇到了微信会自动缓存页面静态资源的问题,必须要手动刷新页面才行,有时候还得刷新好几遍才可以,有些极端情况则是短时间内连续刷新依然显示旧页面,这个问题在公司内一些同事的手机上均出现过。学习了周老师的这节课,对缓存有了基本的了解,明天就用Charles抓包,看看微信内对网页的客户端缓存策略是什么。

第18讲

来自@Jxin

这里想补充一下QUIC相对于TCP的两点内容:

  • 自定义重传机制: TCP是通过采样往返时间RTT不断调整的,但这个采样存在不准的问题。第一次发送包A超时未返回,第二次重发包A,这时收到了包A的响应,但TCP并不能识别当前包A的响应是第一次发送,还是第二次重发返回的,这时不管怎么减,都可能出现计时偏长或过偏短的问题。而QUIC为每次发送包都打了版本号(包括重发),所以可以很好地识别返回的包是哪次发送包的,进而计算也就相对准确。
  • 自定义流量控制: TCP的流量控制是通过滑动窗口协议,是在连接上控制的窗口。QUIC也玩滑动窗口,但是力度是可以细分到具体的Stream。

其实应用层的协议多种多样,比如直播的RTMP、物联网终端的MQTT等,但感觉都是两害取其轻的专项优化、对症下药的方案。只有QUIC直面了TCP的问题,通过应用层的编码实现,系统地提供更好的“TCP连接”。

第19讲

来自@zhanyd

在网上看到华为云CDN主要的应用场景,希望借此可以帮助我们更好地理解内容:

  • 网站加速:CDN网络能够对加速域名下的静态内容提供良好的加速服务。支持自定义缓存规则,用户可以根据数据需求设置缓存过期时间,缓存格式包括但不限于zip、exe、wmv、gif、png、bmp、wma、rar、jpeg、jpg等。
  • 文件下载加速:适用于使用http/https文件下载业务的网站、下载工具、游戏客户端、App商店等。
  • 点播加速:适用于提供音视频点播服务的客户。通过分布在各个区域的CDN节点,将音视频内容扩展到距离用户较近的地方,随时随地为用户提供高品质的访问体验。
  • 全站加速:适用于各行业动静态内容混合,含较多动态资源请求(如asp、jsp、php等格式的文件)的网站。

第20讲

来自@zhanyd

为什么负载均衡不能只在某一个网络层次中完成,而是要进行多级混合的负载均衡?

因为每一个网络层的功能是不一样的,这样就决定了每一层都有自己独有的数据,在不同的网络层做负载均衡能达到不同的效果。比如要修改MAC地址,在数据链路层修改最方便,要修改IP地址最好在网络层修改。

关于网络分层,我这里也打个比方。小帅在网上下单买东西,卖家需要寄快递,把要寄的商品(物理层)打包到包装盒里(数据链路层),然后把包装盒放到快递盒子里(网络层),在快递单上写上寄件地址和收件地址(Headers)。

然后快递员打电话给小帅拿快递(传输层),这里出现了TCP三次握手连接:

  • 快递员:“喂,这里有你的快递,麻烦到门口拿一下”。
  • 小帅:“好的,我这就过来”。
  • 快递员:“那我在门口等你”。

小帅拿到快递后,在网上点击确认收货按钮,确认收货(返回 http Status Code 200,应用层)。

第22讲

来自@zhanyd

“能满足需求的前提下,最简单的系统就是最好的系统”。这句话的隐藏前提是,我们的选择空间要足够大。不管是CDN、负载均衡、客户端缓存、服务端缓存,还是分布式缓存,都给我们提供了大量的选择余地,可以根据自己系统的实际情况,灵活地选择最适合的方案。

这句话的另一种理解是:没有最好的方案,只有最合适的方案。

第25讲

来自@zhanyd

角色是为了解耦用户和权限之间的多对多关系。比如有100个用户,他们的权限都是一样的,如果给每个用户都设一遍权限,这就太麻烦了,而且还很容易出错。这时候设置一个角色,把对应的权限配置到角色上,然后这100个用户加到这个角色中就行了。

角色还有一个好处,如果角色的权限变了,所有角色中用户的权限也会同时变更,不用一个个用户去设置了。

许可是为了解耦操作与资源之间的多对多关系,比如有新增用户、编辑用户、删除用户的三种操作,通常这些都是一起的,要么都能操作,要么都不能操作。这时候就可以把这三种操作打包成一个用户维护许可,用许可和角色关联更简洁。

关于Spring Security中的Role和Authority我是这么理解的:Role就是普通的角色,拥有一组许可,这个角色下的所有用户的权限都是一样的。

但是如果一个角色中的一些用户有个性化的需求,比如销售助理角色,本来没有查看客户的权限,但是某个销售助理比较特殊,需要查看客户的信息,这时如果是单角色的系统,就需要新增一个“销售助理可查看客户角色”,这样很容易导致角色数量爆炸。

而有了Authority,就可以满足这种个性化需求,只要把查看客户的权限加到Authority中赋予用户就行了。

第26讲

来自@zhanyd

Cookie-Session就相当于是坐飞机托运了行李,只要带着登机牌就行了。但是一旦托运了行李,行李就和飞机绑定了,你就不能随意换航班了。

JWT就相当于是坐飞机拎着行李到处跑,每次过安检还要打开行李箱检查,而且箱子太小也带不了多少东西。但它的优点是可以随意换航班,行李都在自己身边。

参考资料

https://learn.lianglianglee.com/%e4%b8%93%e6%a0%8f/%e5%91%a8%e5%bf%97%e6%98%8e%e7%9a%84%e6%9e%b6%e6%9e%84%e8%af%be/%e6%98%a5%e8%8a%82%e7%89%b9%e5%88%ab%e6%94%be%e9%80%81%ef%bc%88%e4%b8%8b%ef%bc%89_%20%e7%a7%af%e7%b4%af%e6%b2%89%e6%b7%80%ef%bc%8c%e7%9f%a5%e8%a1%8c%e5%90%88%e4%b8%80.md