「读」架构真经 | 互联网技术架构的设计原则

 
架构的思想是非常宝贵的,设计的基本原理不会因为新技术的层出不穷而过时。怎样以最低成本最大化系统的扩展性?怎样达到风险利益的平衡点?答案尽在本书中。

第一章 大道至简

本章围绕着简化这个主题,从需求到设计、实施、部署再到网络设备。

需求方面的过度设计比较容易避免,控制好项目范围和需求范围就可以了。

想要避免设计方面的过度设计,最简单的方法是把设计的解决方案展示给其他技术团队,要求其他团队能够快速轻松地理解,如果任何一个团队不理解这个解决方案,就应该思考下,是否设计得过于复杂。所以归根结底,我们在设计解决方案时,首要考虑的是目前业务的增长速度和需求,只做当下最好的设计。

我想起自己一个实例,一个小需求——登录注册页底部加广告位。遗憾的是在兼容性测试中发现,Android 4.4 以下设备,底部广告位不能和底部的软键盘友好兼容,出现了广告位变形的情况。(且实践后发现,需要针对 Android 4.2 和 4.3 做不同的兼容),采取了一系列解决方案后,我意识到不应该为低版本用户做这种可能会影响性能的特殊监听,方案的复杂度也已经超出控制了。立马和产品协商,最简单干脆的方案是低版本用户不显示该广告位,问题至此解决。

那是不是简单的设计,就意味着不需要考虑系统未来的扩展性?当然不是!我们可以选择一开始就设计好扩展方案,等待需求规模的增长,再来实施这个扩展方案。虽然一开始就设计付出的智力成本比较大,但我们没有付出更大的技术和资产成本,智力成本处于可接受的成本范围。并且在现有的方案下,我们可以随时快速参考,随时快速实施扩展。

 

 

在实施过程中,推荐使用被广泛采用的开源或第三方解决方案,最简单的实施几乎总是那些有过实践经历并通过实践证明了的可扩展方案。想想如果采用开源库,成千上万的开发者一起使用,同时找bug,且有专人维护和更新。这些都是自己创建方案所不能做到的。当然,如果有商业库,最好还是采用商业方案,任何一个人都不可能是各方面的专家,商业供应商拥有该方案领域的专家(如
OCR 或者推送),我们通常能得到一个高质量的方案,这个方案不单单是高质量的编程,而且是该领域的高性能。

第二章 分而治之

本章围绕着扩展这个主题,提出了三个简单规则:x 轴扩展、y 轴拆分、z 轴拆分。

 

 

你可以注意到,x 轴是扩展,y 轴和 z 轴都属于拆分,进而达到扩展的目的。

成本最低的就是 x 轴扩展,主要手段是复制数据库和服务,来分散高频事务处理带来的负载,CDN 负载均衡就是这种规则的体现。

随着数据规模逐渐扩大,数据复制可能会出现瓶颈,这个时候,我们可以着手于扩展 y 轴,也就是把数据或服务按名词(以资源为导向,如:用户信息、产品信息)或动词(以服务为导向,如:注册、登陆、搜索)标识拆分。随着拆分,我们庞大的系统拆分成为子系统,团队也可以随之拆分,类似于目前公司的垂直化分组,每个小团队专门负责不同的子系统,更加专业的同时也提高了生产力。

当数据集的用户基数(此处以用户举例,实际上什么都可以进行拆分,思路一致,找到共同点与不同点即可)越来越大,且用户属性有明显不同时(如:地区、行业等),可以考虑 z 轴拆分。书中介绍到:可以根据用户属性分块,在应用发布时,可以通过先发布到含有少量客户的一小块来控制风险,没有问题后,再发布给其他大块或者全量。这就有点类似于 Android 灰度包的做法,只不过灰度包中,是用注册渠道(小米渠道、华为渠道)来标识用户块。我们可以把几个非常小的渠道合并为一个分块,减少数据块的碎片化。说到底,z 轴可以把数据分割成容易命中的多块数据,避免数据集过大,需要长时间遍历的弊端。

第三章 水平拆分

向外扩展(复制或拆分数据,分散负载),而不是向上扩展(购买更大的硬件来支撑)。

通俗讲,就是小、简单、多优于大、复杂、少。硬件再怎么大,都有个上限,采购成本也随之指数上升。如果是向外扩展,设备可以随时被替换或丢弃,扩展可以无限。

当我们做容灾时,通常会考虑双机备份。本文提出一个非常棒的设计方案——三活数据中心。如果想快速扩展现有的双数据中心,可以加个云数据中心来作为第三个数据中心,最小化了硬件支出。但是这个方案也不是全是优点,考虑下数据的同步频率,额外的连接(N中心就有(N*(N-1))/条双向连接),当数据中心不断扩展时,这个复杂度还是很可怕的。

 

三活数据中心

 

三活数据中心 + 三个热数据中心

 

最后,利用云技术来处理意外、临时、突发或偶发的需求。可以有效降低硬件成本,提高我们的响应速度,也减少了我们改变产品需求的风险成本。

第四章 先利其器

本章主要讲的是开发中工具使用的思考,包括:怎样选择数据库?哪种数据库更适合?数据存储是不是非数据库不可?防火墙的意义是什么?是否实施防火墙的决定因素有哪些?日志文件怎么有效发挥作用?

在了解这些开始前,我们需要避免陷入工具法则,技术解决方案可以尝试多方案,平时和不同技术栈的同事多交流,花时间做技术调研,学习新事物,主动分析、实验、采用新工具同时不断革新工具,使用最适合的工具,避免被自己只熟悉的东西困住。

日志文件这块,我想以客户端的角度详细讲讲。

使用日志的第一步,是要解决收集日志的问题。收集可以用 AOP 来插入日志调用语句,也可以结合业务逻辑手动来调用。那么什么时候上报?我们可以间隔一段时间,就上报,如果考虑到服务器的负载,也可以下次启动时统一上报。这种情况下,考虑下如果日志文件太大,上传过程中进程被杀,我们需要支持断点续传。上报的具体时机,按实时性的需求选择。

第二步上报之后,服务器需要把日志聚合到日志服务器上统一存储。服务端需要提供完整的日志分类、即时报表统计工具、监控支持和支持全文检索日志。

下一步是日志分类,是按用户维度?还是按日志等级维度?日志中的错误,可以按机型或者人数来统计,报错多的类似日志,我们优先排查解决。

每天产生这么多日志,维护和存储成本逐渐上升,最新的数据价值最高,我们考虑对旧数据进行归档或删除。

最后书中写道:引进每项新技术都需要另外一种技能来支持。尽管工作中使用合适的工具很重要,但是不要过度强调专业化,以至于没有足够深度的技能来支持。

共勉。

第五章 画龙点睛

本章围绕着系统扩展性主题,罗列出限制了系统扩展能力的错误设计。

首先是不要复查刚插入的数据——这种成本翻倍又难以维护的操作。

其次是滥用重定向,这会降低用户体验,影响页面搜索引擎排行。

最后,因为大多数关系型数据库不擅长保持节点之间数据的一致性,所以没有必要为了场景的一致性,影响了数据库的分布式扩展。合理地放宽时间约束,找到一个系统方便扩展,用户又容易接受的时间点。

不管做什么,首要考虑都是最小成本产生最大效益。我们允许某些不同步的小错误换取扩展性的最大化。

第六章 缓存为王

想提高扩展性,缓存是个很好的手段。从浏览器到网络、知道应用和数据库每个层次,缓存有无数选项可以考虑:

  1. 通过 CDN 缓存来平缓请求高峰和增长,提高服务器负载。
  2. Ajax 提供了丰富的异步动态交互,但需要注意频繁的废请求。
  3. 在网络服务器前面实施页面缓存。
  4. 根据数据读取的边界或者相关性,进行 y 轴 z 轴拆分,从而提高缓存的命中率。
  5. 推荐对 Sql 数据集建立对象缓存层,既不影响服务器性能,又方便独立地扩展缓存池。
  6. 推荐通过 HTTP 头来控制缓存,如果数据结果没有包含用户的隐私数据的话,指定 Cache-Control 头为 public有利于数据结果可以缓存在从客户端到服务器之间的任何代理及缓存。(如:浏览器、CDN、页面缓存、应用缓存)

第七章 前车之鉴

本章提出了一个有趣的观点:构建高可用性和高可扩展性的系统,目的就是防止频繁失败,因此可以学习的机会也比较少。经常失败的组织往往有更好的学习和成长机会,如果他们能抓住机会并从中学习。

引申开来,在敏捷软件开发实践中,每个 Sprint 结束后有回顾总结会,主要议题就是:在这个 Sprint 里,我们哪些做的比较好的,哪些是需要提高的,下个 Sprint 要采取哪些措施。这个回顾总结的本质就是复盘,目的是让团队从过去学习,来提升团队的整体交付能力。

最后本章还指出,需要确保所有版本的代码都有回滚能力,回滚的成本远远低于发布引起的线上故障,也能把风险降低到可控范围。客户端的话,效果类似于热修复,都是适用于线上紧急情况下实时高效把损失降到最低。

第八章 重中之重

本章深有体会的一点:实体间的关系影响了我们如何存储、检索、更新数据和扩展拆分数据。数据的完整性和规范程度越高,关系越紧密,就越难以扩展和拆分,我们需要折中考虑。

第九章 有备无患

本章重点在于提高系统的高可用性,系统的合理故障处理与隔离。

可用性和可扩展性具有同等重要性,可用性不高的系统不需要扩展,不能扩展的系统也不会是高可用性的。通过减少故障和频率和影响范围,我们可以提高系统的整体可用性。

减少故障的手段有以下几种:

  1. 用泳道隔离故障,各泳道之间不共享资源特别是数据库和服务器,并禁止不同泳道之间同步调用,避免故障阻塞,如果需要调用,推荐采用异步。异步的调用非常类似于观察者的模式,仅仅把事件传递出去,而观察者有没有收到,是否处理了,被观察者并不关心。我们可以沿 y 轴或 z 轴进行拆分,这有利于故障隔离,不同服务间彼此独立,有助于我们快速定位问题,缩小排查范围。
  2. 当使用单例模式时,就要慎重小心了。如果多个系统都需要共享这个单例,一旦单例失败,则会引起系统范围的故障。
  3. 并联而不是串联系统(除非有多版本子系统可以随时取代),避免累积影响。
  4. 增加启用/禁用框架,可以智能或人为干涉开启/关闭服务。这类似于开关的概念,在功能上加开关来做到一款上线/下线,防范风险。

第十章 超然物外

本章论证了引入状态会给我们系统带来多大的麻烦:耗费内存和处理能力、依赖增加、故障时状态无法恢复。

所以我们不惜一切代价避免状态。如果状态是必要的,建议把数据存储在用户端(如浏览器中的 Cookie 机制、客户端的 Token)因为存储在用户端,所以需要做好加密,防止中间人窃取凭证伪装用户。服务端虽然可以减少存储、检索成本,但保险起见,需要有校验的步骤,不可完全相信用户端传来的凭证。

如果有些设计必须要把状态数据存在服务端,就用分布式缓存来作为单层中间层处理。这里需要考虑下分布式中更新状态数据和读取状态数据的冲突问题,以及数据需不需要持久化。

第十一章 异步通信

本章讨论了异步通信的准则和处理。可以说在开发中特别是客户端开发中,由于主线程的特性,异步思想是无处不在。

异步一般可以借助回调或者事件总线(本质上就是观察者模式)来完成,如果嵌套层次太深,或者需要解耦,或者遇到跨服务的情况,就要考虑用事件总线了。

本章引发我对事件总线扩展性的一些思考(毕竟单用户的客户端只需要一条总线就足够了):

  1. 这么多的服务和系统,用同一条总线,高频的读写会引起堵塞,怎么扩展总线?最直接的方式就是按照面向的服务和属性拆分(y 轴),这会牺牲一些灵活性,你不能够跨服务去通知;按照客户边界拆分成泳道(z 轴),泳道之间也可异步通信。


 

  1. 总线是否可以扩展下优先级属性,在同时发生事件时,优先级高的事件优先通知。
  2. 不需要再处理的观察者,需要增加自动解绑机制。

第十二章 意犹未尽

我们怎么应对突发的流量?怎么提高系统高峰时的可用性?

有两方面可以解决:

  1. 利用云或者应急的容量,来负载突发请求,同时系统要做好这方面的扩展;
  2. 建立监控体系,存在异常波动,及时报警。

需要说明的是,虽然我们反复强调不要造轮子,优先使用被广泛采用的开源库或第三方解决方案,但是供应商永远都不会像你自己一样,在第一时间处理方案中的 bug 。所以这需要我们做好解耦,评估扩展性和风险点。第三方方案的缺陷我们是否能接受?是否在可控范围?自己的团队是否可以方便地扩展和修改以适应自己的需求?最差的情况下,第三方方案是否容易随时替换?这些都是我们在使用第三方前需要考虑的。

在扩展时,不要依赖供应商的产品、服务或系统功能来扩展,如果依赖于供应商的专有方案,我们将失去主动权和竞争力。就像携程没有开源的React Native优化框架,就是客户端的竞争力之一。

上面第 2 点的监控,架构设计之初就应该纳入考虑,监控系统的状态比如 CPU、内存使用率固然重要,更重要的是从业务指标做监控如注册成功率、下单转换率、搜索频率等,这样才更直观地看到业务的异常情况,具体受到什么的影响。文中提出监控系统一个有意思的展望:使用控制图或者机器学习,来预测是否会发生异常,并支持系统自我修复,这种自动化自愈系统,将是未来的方向。

最后,文章提出了对数据进行梯级存储策略,毕竟处于大数据时代,庞大的数据无疑增加了我们的成本。结合业务,评估数据的价值和访问频率,使用不同的存储介质和策略,比如低价值低频访问率的数据,是删除还是迁移到低速存储中?

我不知道你有没有留意过,跨年的快递单号已经查询不了物流信息了,因为已经签收了的数据,随着时间推移慢慢老化,失去价值,这部分数据完全可以删除掉。

第十三章 谋定而动

本章用风险收益分析方法,评估了全文提出的50条规则。

这个评估方法,也可以运用在我们平时的解决方案选定中:这个方案能降低多少风险,解决什么问题?风险出现的频率怎么样?降低的风险点,对系统和业务的影响面多少,损失会是多少?如果采用该方案,我们的成本怎么样?成本和风险比起来,我们能得到多少收益?这些都是我们需要思考的,毫无疑问优先采用降低风险多又低成本的方案。

 

可扩展性与可用性风险分解

 

 

总结

本书的思想和规则非常宝贵,大概总结了 12 条设计过程中需要遵循的架构原则。

  1. N+1设计。永远不少于两个,通常为三个,当故障发生时,至少保证有一个冗余的实例。
  2. 回滚设计。确保系统可以回滚到以前发布过的任何版本。
  3. 禁用设计。能够关闭任何发布的功能。
  4. 监控设计。在设计阶段就必须考虑监控,而不是在实施完成之后补充。
  5. 使用成熟的技术。只用确实好用的技术,谨慎使用最新技术。
  6. 异步设计。只有在绝对必要的时候才进行同步调用。
  7. 无状态系统。只有当业务确实需要的时候,才使用状态。
  8. 设计至少要有两个步骤的前瞻性。在扩张性问题发生前考虑好下一步的行动计划。
  9. 非核心则购买。如果不是你最擅长的,也提供不了差异化的竞争优势则直接购买。
  10. 小构建,小发布,快试错。全部研发要小构建,不断迭代,让系统不断地成长。
  11. 隔离故障。实现故障隔离设计,通过断路保护避免故障传播和交叉影响。
  12. 自动化。设计和构建自动化的过程。如果机器可以做,就不要依赖于人。
参与了读书计划,所以本书感悟按章分解。
编辑于 02-12
系统架构
技术架构
Java
 

推荐阅读

技术架构汇总

技术架构汇总

五种常见软件架构(转载)

一、分层架构分层架构(layered architecture)是最常见的软件架构,也是事实上的标准架构。如果你不知道要用什么架构,那就用它。这种架构将软件分成若干个水平层,每一层都有清晰的角色和…

从新手到架构师,一篇就够:从100到1000万高并发的架构演进之路

从新手到架构师,一篇就够:从100到1000万高并发的架构演进之路

这是我读过写得最好的【秒杀系统架构】分析与实战!

1 秒杀业务分析2 秒杀技术挑战3 秒杀架构原则4 秒杀架构设计5 大并发带来的挑战6 作弊的手段:进攻与防守7 高并发下的数据安全8 总结《Java 2019 超神之路》《Dubbo 实现原理与源码解析 —…

还没有评论