Web Scalability for Startup Engineers Tip&Techniques for Scaling You Web Application --读书笔记

Web Scalability for Startup Engineers Tip&Techniques for Scaling You Web Application
第1章和第2章讲述可伸缩系统的核心概念与软件设计原则。强烈建议认真阅读这两章,这部分内容包含了开发一个可伸缩的Web系统甚至开发一个良好软件的基本原理和设计原则,是其它一切技巧和方法的元规则。
第9章讲述可伸缩的系统运维及可伸缩的个人和团队,如果你正处在一个高速发展的创业团队中,如果你对从技术走向管理感兴趣,我相信你可以从本章的内容中收获多多。

习惯的旧秩序被互联网打破了;这是一个创新的时代,任何奇思妙想都有被用户接受成为现实的可能

线下的思维转换为线上的思维

互联网催生了一种新型的生活方式,也催生了适应于互联网的一种新的人群。

连接人群和线上生活的纽带就是一个一个的互联网及移动互联网的应用。

作为应用的典型IT形态,互联网应用经历了独占型应用、SOA应用,在云时代就是云原生(Cloud Native)的应用。

构建一个良好的可伸缩的应用,不仅仅是一个优秀工程师的职责,同时也代表了对一种生活方式的认可。

弹性架构的概念,软件设计的原则,以及如何构建一个优质的互联网应用。

一个最初由两三个工程师开发的产品雏形,如何经过逐渐地重构、演化、迭代、伸缩,最终成为一个巨无霸系统?

第一章和第二章讲述可伸缩系统的核心概念与软件基本设计原则,这部分内容包含了开发一个可伸缩的Web系统甚至开发一个良好的软件的基本原理和设计原则,是其它一切技巧和方法的元规则。

可以在工作中遇到问题时快速浏览,寻找方法和灵感。

可伸缩系统的设计是一种权衡的艺术,必须对第一种方案的优缺眯都了如指掌,才能在面对实际问题时做出最合适的选择。

高并发可伸缩系统的设计看似纷繁复杂庞大无比,实际上关键的核心技术也就那么几样,如果深入掌握了这些关键技术,就抓住了可伸缩系统设计的核心。这几样关键技术,可能需要在不同场景,从不同视角反复思考琢磨,才能真正掌握。

成竹在胸

 

核心概念

极具弹性的系统,在快速变化的内外部环境中保持快速响应的能力。

那些在传统企业中需要花费整年时间才能搞定的事情,创业公司可能必须在几个星期内搞定。如果足够成功并且足够幸运,可能需要在几个星期内把你的系统的处理能力扩大十倍。当然,也可能在几个月后又把系统的处理能力缩小回去。

就伸缩性技术本身而言,目前很多公司提供这方面的基础服务,比如亚马逊云、微软云、Google云,以及阿里云,以及阿里云,腾讯云等,这些云服务可以让你完全不必考虑伸缩性方法的问题而只需关注自身的业务需求。

要完全理解伸缩性需要一个渐进的过程。

在阅读过程中把这些基础设施图及架构图再画一次,这不仅有助于理本书的内容,如果只是匆匆一扫而过,可能会忽略很多有用的信息。

什么是伸缩性

伸缩性是指系统可以根据需求和成本调整需求和成本自身处理能力的一种能力。伸缩性常常意味着系统可以改变自身的处理能力以满足更多用户访问、处理更多数据而不会对用户体验造成任何影响。此外,还有一件重要的事情必须提醒工程师们注意,伸缩性不仅需要伸(增强系统处理能力,即扩容),有时候也需要缩(缩减系统处理能力,即缩容)。同时,这种伸缩还必须相对比较省钱和快速。

正如在不同场景下对伸缩性有不同要求,可以在不同维度上对伸缩性进行度量。

伸缩性主要从以下几个方面度量:

  1. 处理更多数据

  2. 处理更高的并发

    度量并发的指标通常是:应用系统可以同时服务多个用户。

    对于一个Web应用系统而言,并发度的意思是最多有多少个用户可以同时访问你的网站而不会感到访问速度变慢。

    由于服务器的CPU数目是有限的,能同时运行的线程数也是有限的,因此处理高并发是一件非常有挑战的事。如果还要同步某些代码的执行以保证数据的一致性,那些这个挑战将变得更加艰难。

    更高的并发也意思着系统需要同时打开更多的连接,启动更多线程,处理更多消息,CPU需要更多次上下文切换。

  3. 处理更高频次的用户交互

    用户交互是指你的系统和你的用户之间的交互频次。这个维度和并发度看起来很像,但是稍有不同。交互频次衡量你的用户和你的服务器交换信息的频繁程度。与交互频次相关的最大挑战是响应得延迟。

    如果应用的交互频次在增长,你就必须让应用响应得更快速,这就要求系统有更快的读写速度进而将系统并发度推到一个更高的高度。

    例如如果你做的是一个Web站点,用户大概需要第15秒或2分钟从一个页面跳转到另一个页面;

    但如果你做的是一个多人交互的移动游戏,用户可能需要每秒种和服务器通信好几次,因此交互频次相对独立于用户并发度。

通常会综合上述三个维度云考量一个系统的伸缩性。一般说来,实现系统伸缩性的过程,扩容比缩容更重要也更常见。

伸缩性和性能密切相关,但它们并不是一回事。

  • 性能更多的是衡量系统处理一个请求或者执行一个任务需要花费多长时间

  • 伸缩性则更多关注系统是否能够随着请求数量的增加(或减少)而相应地拥有适应的处理能力

EG:你有100个并发用户,每个用户平均第5秒发送一个请求,那么系统吞吐量就是每秒20个请求。

  • 性能指的就是系统需要花费多少时间处理这每秒20个请求

  • 伸缩性指的是在不影响用户体验的前提下,系统最多能够处理多少个并发用户及用户最多能发送多少个请求

软件产品的伸缩性也许会受限于软件开发团队的规模。如果要系统具有伸缩性,那么对应的软件开发团队也必须具有伸缩性,否则就没有足够的工程师资源去快速影响需求变更。

尽管看起来软件开发团队的伸缩性和技术无关,但事实上系统架构和设计会影响团队规模。

如果你的系统设计是紧耦合的,所有人都在一份代码上工作,那么你就很难扩展你的工程师团队

由于团队的沟通效率和团队规模成指数关系,因此当完成同一个工作的技术团队的规模达到8-15人时,效率就会变得非常低。

为了更好地体会伸缩性对创业公司的影响,让我们试着从公司视角观察。问你自己:制约我们公司持续发展的问题有那些?

答案不仅包括我们前面提到的处理同吞吐量导致的技术架构方面的问题,也包括开发过程、团队、代码结构等一系列问题。

从单一服务器到全球用户的Web 架构演化

这里展示的很多伸缩性演化架构阶段仅仅在你开始做规划时有参考意义。

很多情况下,真实世界并不完全按照这个方式去演化,更可能会经历多次重写。还有一些时候,系统在设计和诞生之初就处于某个演化的特定阶段并一直保持不变,或者在发现架构制约的时候直接向上跳跃一到两个阶段而不是逐步演化

尽量避免完全重写应用,特别是创业公司。重写总是比你预期的时间要长,也比你预估的难度更大。基于我的经验,一次重写带来的麻烦需要两年才能终结。

单一服务器

除DNS外,网站服务器需要响应各种Web页面、图片、CSS文件和视频文件,所有的响应都只在这一台服务器上处理,所有的网络流量都只经过这一台服务器进行传输。

最便宜的主机方案是共享主机,用户只购买一个用户帐号而没有管理权限,这个帐号和其它客户的帐号安装在一个服务器上。这种主机方案适用于那些只需要一个最小的Web站点或者只有一个着陆页(Loading Page)的网站。但是这种方案限制太多,不值得推荐。

瓶颈:

  • 你的用户在持续增长,因此访问量在持续增长。每个用户访问都会对服务器造成负载压力,服务每个用户又需要更多的计算资源,包括内存、CPU时间,以及磁盘读写(I/O)。

  • 你的数据库存储的数据在持续增长。在这种情况下,数据库由于需要更多的CPU、内存和I/O请求而变慢。

  • 你要扩展系统增加新的功能,这使得每一次用户请求都要消耗更多的系统资源

  • 上面这些因素常常会叠加在一起。

使用更强的服务器:垂直伸缩

一旦你的应用达到服务器的极限(由于网络流量、数据处理规模或者并发度等因素的增长),就必须决定如何去伸缩系统。

有两种不同的伸缩性方案:

  1. 垂直伸缩

  2. 水平伸缩

通过升级硬件和网络吞吐能力可以实现垂直伸缩。由于不需要改变应用架构,也不需要重构任何东西,所以通常被认为是最简单的短期伸缩性方案。

垂直伸缩的方案有很多:

  • 通过使用RAID(独立冗余磁盘阵列)增加I/O吞吐能力。I/O吞吐量和磁盘存储是数据库服务器的主要瓶颈。使用RAID并增加磁盘数量有助于将读写请求分布到多个磁盘上。最近几年,RAID10变得格外流行,这种RAID方案即提供了数据冗余存储又提高数据吞吐能力。从应用角度看,整个RAID看起来就是一个数据卷标,但是在实际底层实际包含了多个磁盘共同对外提供读写访问。

  • 通过切换到SSD(固态硬盘)改善I/O访问速度。随着固态硬盘技术越来越成熟,价格越来越低,SSD也变得越来越流行。根据不同的其准测试方法,SSD随机读写速度大约比传统磁盘快10到100倍。应用通过替换SSD硬盘可以节约更多I/O等待时间。不过,顺序读写的速度提升就没有这么高了,所以现实中应用性能提升也没有特别巨大。事实上,大多数开源数据库(比如MySQL)会优化数据结构和算法,尽可能多地执行顺序硬盘操作而不是随机操作。而某些数据存储系统,比如Cassandra,做的更彻底,所有的写操作和多数读操作都只使用顺序I/O操作,这也使得SSD更缺乏吸引力。

  • 通过增加内存减少I/O操作(如果你的应用部署在独立的物理服务器上,128GB内存是一个比较实惠的常规配置)。增加内存意味着文件系统有更多的缓存空间,应用程序有更多的工作内存。此外,内存大小对于数据库服务器效率的提升也格外重要。

  • 通过升级网络接口或增加网络接口提高网络吞吐能力。如果你的服务器需要处理大量视频等媒体内容,也许需要升级网络供应商连接,甚至升级网络适配器以获得更高的吞吐能力。

  • 更新服务器获得更多处理器或者更多虚拟核。拥有12或者24线程(虚拟核)的服务器是一个比较实惠且合理的升级方案。CPU和虚拟核越多,能够同时执行的进程数就越多,系统因此变得更快,这不仅仅是因为多个进程可以不必共享同一个CPU,还因为操作系统不需要在同一核执行多个进程而执行不必要的上下文切换。

垂直伸缩的一些严重的制约:

  1. 成本。当越过某个点后,垂直伸缩会变得格外昂贵。

  2. 垂直伸缩是有极限的,这是个比较大的问题。无论你愿意花多少钱,内存都不可能无限地增加下去。类似的限制还有CPU的速度,每台服务器的虚拟核数目,硬盘的速度。简单的说,到了某个极限,没有任何硬件能力能够继续增加。

  3. 操作系统的设计或者应用程序自身制约着垂直伸缩最多只能达到某个点。举个例子,你不能通过增加CPU数目而无限增加MySQL的处理能力,因为同时锁竞争也会增加(特别是如果你使用MyISAM这种比较老的MySQL存储引擎)

如果多个线程共享诸如内存、文件这类资源时,需要用锁进行同步访问。低效的锁管理会导致锁竞争成为瓶颈。

应用操作应该使用细粒度的锁,否则,应用需要花费很长的时间去等待释放锁。一旦锁竞争成为瓶颈,增加更多的CPU核数也不会改善应用的处理能力。

高性能的开源应用或商业应用可以扩容到几十个CPU核,然而,在购买新硬件前最好还是要确认下系统的扩容伸缩能力。由于高效的锁管理是一项有挑战的任务,需要大量的经验和详细的调试,所以自己开发的应用通常在锁竞争方面做的差一点。在一些极端的情况下,因为在设计之初就没有考虑任何高并发的场景,结果导致增加CPU核对应用处理能力提升没有任何帮助。

垂直伸缩不会对系统架构产生任何影响。你可以垂直伸缩扩容任何一台服务器、网络连接或者路由器而无须修改任何代码或者重构任何东西。唯一要做的就是用更强大更快速的硬件替换现有的硬件。

单一服务器,但是是更强大的服务器

服务分离

服务分离背后的核心理念是你应该将整个Web应用切分成一组不同的功能模块,然后将它们独立部署。这种基于功能将系统划分成独立可伸缩的模块的方式称为功能分割

另一个简单的方案是通过在不同的物理机上安装不同类型的服务,使得一个系统的不同部分被分离部署在不同物理服务器上。要这种场景下,一个服务可以是一个类似Web服务器这样的应用(比如Apache)或者是一个数据库引擎(比如MySQL)。这就使得应用服务器和数据库分离到不同的服务器上。

类似地,也可以把FTP、DNS、缓存及其它服务器部署在不同的物理机器上。

对单一服务器部署进行可伸缩扩容,对可分离的服务进行分隔部署是一种比较轻的解决方案。不过,这种伸缩方案并不能一直持续进行下去,一旦你的所有服务类型都已经分别部署在独立服务器上,就没有办法用这种方法扩容下去了。

缓存是一种重要的服务,目标是通过快速返回提前生成好的内容降低请求响应延迟。缓存是构建可伸缩系统的一种重要技术。

服务器通过在第三方数据中心,数据中心由一组安装不同功能的服务器组成。每个服务器都承担不同的角色,比如Web服务器、数据库服务器、FTP、缓存等。

对单一服务器部署而言,服务分离是一种巨大进步。相比以前,可以将负载压力分摊到更多的服务器上,而且可以按需进一步对每个服务器进行垂直伸缩。

如果某个微型网站受欢迎,访问量大,就会把它分离出来,部署在一台独立的Web服务器和一台独立的数据库服务器上。

一个Web应用利用功能分割将负载分布在更多服务器的场景。应用的每个部分都使用不同的二级域名,这样就可以基于Web服务器的IP地址进行流量分发。

不同的功能模块也许部署在不同的服务器上,这些不同的功能模块也会有不同的垂直伸缩需求。显然,一个系统越是能够分割成不同的部件,每个部件就越有弹性越好。

CDN内容分发网络:静态内容的伸缩性

随着应用不断发展用户不断增长,可能通过CDN服务减轻应用网络流量负载压力。

CDN的全称是Content Delivery Network,即内容分发网络。其基本思路是尽可能避开互联网上有可能影响数据传输速度和稳定性的瓶颈和环节,使内容传输的更快、更稳定。

CDN是一种提供静态文件(比如图片、JavaScript、CSS及视频)全球分布的服务。它的工作原理有点像HTTP代理。用户如果需要下载的图片、JavaScript、CSS或者视频内容,可以通过连接CDN服务器下载而不是连接应用服务器。如果CDN服务器没有用户需要的内容,CDN服务器会请求服务器获取这部分内容然后再缓存到CDN服务器上。一旦文件缓存在CDN服务器上,后面的用户就再也不需要连接应用服务器了。

通过将应用集成到某个CDN服务,可以显著减少应用服务器需要的网络带宽。可以用更少的Web服务器提供Web应用静态内容。CDN会从距离用户最近的服务器提供静态内容,进而加速这些用户的页面加载时间。

我们不是一定要增加很多的服务器或者学习如何对HTTP代理进行伸缩。我们只需要简单地使用第三方的服务,然后依赖它提供的伸缩性能力就可以了。虽然这看起来有点像"伸缩性游戏的小把戏”,但它真的是非常强大的手段,特别是在创业公司的早期开发阶段,可能根本就没有足够的时间和金钱去调查这些技术。

分散访问流量:水平伸缩

水平伸缩是指通过增加服务器提升计算能力的一类架构方法。

水平伸缩被认为是伸缩性的圣杯,水平伸缩可以克服垂直伸缩带来的单位计算成本 随着计算能力增加而迅速飙升的总是。另外水平伸缩总是可以增加更多服务器,这样就不会像垂直伸缩那样遭遇到单台服务器的极限。

水平伸缩可以通过修改应用架构在后期进行“添加”,但是大多数情况下,必须付出相当的开发代价。

一个真正意义上的可伸缩系统不需要很强大的服务器,甚至相反,它们运行在大量的廉价商业服务器上,而不是少量的强大的服务器上。

水平伸缩技术带来的好处要在系统发展的后期才能体现出来。一开始,水平伸缩因为技术比较复杂需要更多的工作量,会花费更多的成本。

  1. 这些成本体现在可水平伸缩的架构比基本的架构需要部署更多的服务器

  2. 水平伸缩的架构需要更有经验的工程师去构建并维护它们

不过,一旦你的系统的计算能力达到某个点,水平伸缩就变成更好的策略。

使用水平伸缩,可以不必花费购买顶级服务器的高昂的价格,也不会触及垂直伸缩会出现的天花板(买不到更强大的硬件)

水平伸缩使用CDN这样的第三方服务不仅更节约成本,而且更透明。

云服务商愿意为高流量的客户提供更低的价格,因为这些客户之前的付费已经覆盖了必要的维护集成成本。对于访问量很大的客户,他们也确实很在意价格高低。

具有水平伸缩特性的系统和先前架构演化阶段提及的系统的不同之在于:

数据中心的每一种服务器角色都可以通过增加服务器进行扩容伸缩

事实上,在不同演化阶段可以部分地实现水平伸缩,有的服务实现水平伸缩,而有的服务则不实现。

达到真正意义上的水平伸缩很困难而且需要丰富的经验。因此,构建水平伸缩的系统先从那些容易做到的地方做起。比如Web服务器、缓存;暂时难以做到的地方,比如数据库及其它的持久存储

在演化的这一阶段,一些应用使用轮询DNS服务实现Web服务器流量分发。

轮询DNS不是实现Web服务器流量分发的唯一手段

 

轮询DNS是DNS服务器的特性之一,它允许将一个域名解析到多个IP地址中的一个。一般的DNS服务器会将一个域名解析到一个单一的域名。然而轮询DNS允许将一个域名映射到多个IP地址上,每个IP地址都指向不同的机器。因此,每次用户请求域名解析,DNS都会返回这些IP地址中的一个。目的就是将每一个用户访问的流量分发到Web服务器集群中的某一台上,不同的用户在不知情的情况下连接到不同服务器上。一旦用户接收到一个IP地址,他就会只与这台选中的服务器通信---相当于服务器是有状态的,如果服务器宕机也会有其它的总是,服务器集群上访问压力也会不均匀。

服务全球用户的伸缩性架构 GeoDNS服务

架构演化的最后阶段就是打造一个全球最大的Web站点,实现全球用户的可伸缩性。一旦你服务的用户从几百万扩展到全球,你需要的数据中心就不止一个。一个数据中心可以部署很多的服务器,但是其它大洲用户的体验可能并不好,而且多个数据中心也能让你比较从容地应对那些可能的宕机事件(水灾、火灾引起的停电)

服务全球用户需要面临很多挑战,用到很多技巧,其中一个技巧是使用GeoDNS服务。

GeoDNS是一种基于客户地理位置进行IP地址解析的DNS服务。一般DNS服务器收到一个域名,比较baidu.com,对台解析成一个IP地址。GeoDNS从用户视角看也是这样,然而,GeoDNS会基于用户的地理位置返回不同的IP地址。一个欧洲用户和一个澳洲用户得到的IP地址可能不同,结果是每个用户都会访问到距他最近的一个Web服务器。GeoDNS的目标就是将用户分发到离他最近的数据中心进而实现最小的网络延迟。

基础设施层面的另一个扩展是在全球范围部署若干边缘缓存(edge-cache)服务器,进一步减少网络延迟。边缘缓存服务器的使用依赖于应用的自然特性。边缘缓存服务器最高效的用法是像反向代理服务器那样缓存整个页面。当然,边缘服务器也会提供其它服务。

边缘缓存是一种距离用户较近的HTTP缓存服务器,便于部分缓存用户的HTTP流量。从用户浏览器发起的请求到达边缘缓存服务器,边缘缓存服务器决定从缓存中直接返回响应页面,还是通过发送背景请求到Web服务器获取响应页面的其它部分最后进行组合。

边缘缓存服务器还可以决定某个页面是不可缓存的,也可以决定是否可以代理整个Web服务。边缘缓存服务器可以缓存整个页面也可以缓存页面片段。

随着应用未来的持续发展,你也许会考虑将主数据中心切分成多个数据中心并把它们部署到离用户较近的位置。通过将应用和数据存储部署到离用户较近位置的方式,可以实现更短的访问延迟和更少的网络成本。

单个数据中心如何支撑可伸缩性

数据中心基础架构高层概览1-10

  1. 前端:应用栈的第一个部分,包含了直接与用户设备交互的一系列组件。前端可能在数据中心内,也可能在数据中心外,这要看部署细节和第三方服务的使用情况。这些组件不包含任何业务逻辑,主要目的是提升系统处理能力和伸缩能力。

    负载均衡器是一种软件或者硬件组件,可以将访问某个IP地址的访问流量分发到多个服务器上,这些服务器则隐藏在负载均衡器的后面。负载均衡器可以将用户访问负载平均地分发到多个服务器上,并且允许动态地增加或者移除服务器。由于用户只能看到负载均衡器,所以可以在任何时候添加新的Web服务器而无须停止服务。

  2. Web应用层:整个应用栈的第二层是Web应用层,由Web应用服务器集群构成,主要职责是处理用户的HTTP请求生成最后的HTML页面。这些服务器通常用轻量级的Web开发框架(PHP、JAVA、Ruby、Groovy等)构建,实现最小的业务逻辑,主要任务是生成用户界面。整个Web应用层的主要用途是处理用户交互并转换成内部服务调用。这个Web层越简单功能越少,整个系统越好。复杂业务逻辑应该在Web服务层完成,实现更好的复用及减少需求变更,因为展示层的变更是最频繁的【与用户交互的Web页面或App】.

  3. Web服务层:应用栈的第三层是Web服务层,由各种Web服务组成。这是非常关键的一层,包含了最主要的应用逻辑。展示层剥离出来的逻辑,就集中在这一层。高内聚的业务逻辑,更容易实现功能分隔;功能分隔后的服务,可以独立地对它进行伸缩。譬如电子商务应用中的产品类目服务和用户信息服务是有完全不同的伸缩性需求的。

  4. 附加组件,对象缓存和消息队列。

    对象缓存服务器既在前端服务器使用,也在Web服务中使用,主要目的是减轻数据存储服务器的负载压力及通过对部分数据进行预先计算实现响应加速。

 

消息队列被用来 将某些处理延迟到稍后的阶段处理,并且将处理操作委托给消息处理者服务器。发送给消息队列的消息通常来源于前端应用及Web服务,然后这些消息被特定的消息处理者机器处理。

 

定时任务,不会去响应用户请求,它们是离线的作业处理服务器,提供类似异步通知、订单处理,以及其它一些允许较高延迟的功能。

  1. 数据持久层:一般来说,这是最难以进行水平伸缩的一层。

数据中心基础架构


图1-10中数据中心基础架构鸟瞰图中各组件的布局是经过深思熟虑的,主要目的是帮助那些相对比较慢的降低负载访问压力。

只有在非常必要的情况下,Web服务层才会连接搜索引擎及主要数据存储去读写必要的信息。

有一点需要特别注意,就是没有必要为了伸缩性实现图1-10中的所有组件。相反,要尽可能少地使用不同种类的技术,因为每增加一种技术,就是在增加系统的复杂度,也是在增加维护的成本 。使用很多不同的技术看起来很酷,但是却让发布、维护、调试变得更困难。

如果应用只是需要一个简单的搜索功能,也许只要一个前端服务器和一个搜索引擎集群就能够满足所有的伸缩性需求。

如果能增加服务器实现现有的每一层的伸缩性,并且也能满足全部的业务需求,为什么还要不厌其烦地使用各种额外的组件呢?

应用架构概览

应用架构是将业务逻辑放在应用架构的核心位置,是关于业务模型的演化,不是关于某个框架或者某个特定技术,也不是关于java、PHP、PostgreSQL或者数据库表结构的。

业务需求驱动着各种决策。

没有正确的领域模型和正确的业务逻辑,数据库、消息队列及Web框架都没有任何意义。

不管应用是一个社交网站,还是一个药品服务,或者是一个视频APP,它总归是有某些业务需要一个领域模型。通过将这个模型放到架构的核心,确保各种组件围绕这个核心展开,服务于这个业务,而不是其它什么东西。

如果把技术放在核心位置,也许会得到一个很赞的Rails应用,但不太可能得到一个很棒的药品服务应用。

市面上已经有很多不错的关于领域驱动设计和软件架构的书,可以帮助更好地熟悉软件设计的最佳实践。

领域模型是关于应用要解决的业务问题的本质描述。

领域模型表示一个应用的核心功能,重点在于业务而不是技术。领域模型解释了关键术语、角色和操作,而不关心技术实现。

一个自动柜员机(ATM)的领域模型的关键词是:现金、帐户、负债、信用、身份认证、安全策略等。

同时,领域模型不关心硬件和软件实现。

在系统内部,应用被分解成多个(高度自治的)Web服务。

应用架构高层鸟瞰图1-11

应用架构的核心是主要业务逻辑

  1. 前端:主要职责是成为用户的接口,用户通过网页、移动APP或者Web服务调用完成和应用的交互。是介于公开接口和内部服务调用的处理层。

    前端可以被视作是应用的“皮肤”或者应用的插件,是系统向用户呈现的功能展示,因此不应该是系统的核心或者重点。

    一般来说,前端应该尽可能保持简单。

    通过保持前端简单,可以复用更多业务逻辑。

    服务的简单与职责单一,使服务只关注逻辑而不是视图呈现。

    业务逻辑只存在Web服务层,可以避免视图和业务强耦合的问题,并且可以实现前端的独立伸缩,即只关注处理较高的并发访问请求。

    可以认为前端是一个可移除的插件,可以用不同的语言重写、可以输入各种类型的前端。

    可以移除一个基于HTTP的前端输入而插入一个移动应用的前端或者一个命令行前端。

    即前端展示要与核心业务逻辑分离。

    前端不应该关注数据库或第三方服务。允许前端代码出现业务逻辑会导致代码难以复用及系统更高的复杂度而难以维护。

    可以允许前端发送事件消息给消息队列,以及允许使用后端缓存,消息队列和缓存都是提升性能和改善伸缩性的重要手段。

    无论是缓存整个HTML页面还是HTML片段,都会比在构建HTML时缓存数据库查询更节省处理时间。

  2. Web服务:处理主要流程和实现业务逻辑的地方。要点服务职责单一,方便复用和伸缩。

    SOA就像雪花,不会有两个完全相同。--David Linthicum

    Web服务在整个应用架构中处于中心位置,这种架构通常被称为面向服务的体系架构(SOA)。

    SOA是一种以低耦合和高度自治的服务为中心的软件架构,主要目标是实现业务需求。

    SOA倾向于所有的服务都基于有清晰定义的约定,并且都使用相同的通信协议。

    在SOA定义中,不太关注SOAP、REST、JSON或者XML,这都是实现上的细枝末节而已,不论使用什么技术或者协议,只要你的服务是松耦合的并解决了一组特定的业务需求就可以了。

    SOA不是所有问题的唯一答案,还有其它一些架构:比如分层架构、六角架构和事件驱动架构。

    分层架构

    分层架构:是一种将不同功能划分到不同层次的架构。低层的组件暴露一组API给高层组件使用,不过低层组件永远不会依赖高层组件提供的功能。

    分层架构的一个例子是操作系统及其各种组件。

    每一层都消费其低层提供的服务,但是低层永远不会消费上层提供的服务。

    另一个比较好的例子是TCP/IP编程栈,每一层都依赖低层提供的协议并增加新的功能。

    分层可以强制结构化并减少耦合,低层组件变得更简单和系统,其它部分更少耦合。替换低层组件只要实现相同API就可以了。

    分层构架的一个重要影响方面是越到底层稳定性越强。

    变更高层组件的API很容易,因为依赖它们的东西很少。但是变更低层的API就不划算了,因为有大量代码依赖这些已经存在的API。

    六角架构

    六角架构认为业务逻辑是架构的中心,所有数据存储、客户端、其它系统之间的交互都是平等的。业务逻辑和每一个非业务逻辑组件之间都有一个约定,但是没有底层和顶层的划分。

    在六角架构中,用户和应用的交互 与 应用和数据库系统的交互没有区别。它们都存在于应用业务逻辑之外而且都遵循一个严格的约定。定义好这些边界,就可以用一个自动化测试驱动代替一个人 或 用某个存储引擎代替数据库系统而不会对系统核心造成任何影响。

    事件驱动架构(EDA)

    简单地说,是以一种不同的方式去思考行动,即 对已经发生的事件做出反应。

    传统编程模型中,我们认为我们是请求某项工作要完成的人

    譬如,createUserAccount(),我们期望在我们等待结果的过程中,这个操作会被执行完成,

    然后我们继续后面的过程。

    EDA中,我们不会等待事情被做完。

    无论什么时候,我们和其它组件交互,我们只是宣布某件事情已经发生,然后就处理后面的过程了。

    所有架构的目的都是将系统切分成更小的独立的功能单位

    目的:构建更高层次的抽象以实现隐藏复杂性、减少依赖、各部分独立伸缩,以及每个部分并发开发

    可以认为Web服务层由若干高度自治的应用组成,每个Web服务自己就是一个应用。

    Web服务也可以彼此依赖,不过说到底还是少互相依赖为好。

    Web服务提供一个更高层次的抽象,可以让它看清整个系统并理解它。

    每个服务都隐藏了自身实现的细节并呈现一个简化的高层次的API。

    Web服务的主要目标: 解决业务需求。 如何实现,只是途径

  3. 支撑技术

消息队列、应用缓存、主数据存储、搜索引擎等,这些通常是用一些其它技术实现,一般都是一些第三方的产品,通过配置和我们的系统集成起来。

数据库(主数据存储)仅仅是一种技术,是实现的细节而已。

从应用架构的视角看,数据存储是一个让我们读写数据的地方

我们并不关心需要多少服务器,如何进行伸缩,怎么进行数据复制及容灾,甚至如何存储数据。

数据存储、缓存、消息队列,都是一种即插即用的扩展组件

如果决定更换持久层存储或者更换缓存后端,应该做到只更换数据连接组件就可以,保证整个架构不受影响。

数据存储不是应用架构的核心,不应该处于系统架构的支配地位。

通过将数据存储的抽象化,可以自由选择各种数据库,不论MySQL还是其它数据库。

如果应用逻辑有不同的需求,也可以考虑NoSQL数据存储或者内存型存储。

第三方服务在我们的控制之外,处于我们系统边界之外。因为我们不能控制它们,我们也就不能指望它们一直动作正常、没有BUG、像我们期望的那样快速伸缩。

通过设计一个间接的访问层,

将第三方服务和它们的系统隔离开来,

可以最大程度地降低使用风险和系统依赖

架构是从软件设计的角度看

基础设施是从系统工程师的视角看

每一种视角都是从不同方面展示同一问题:如何构建可伸缩的软件。

可伸缩Web应用的关键:架构、基础设施、技术、算法、业务需求之间的冲突

 


第二章 软件设计原则

许多实际项目中遇到的伸缩性问题其实可以归因于违反了软件设计的核心原则。软件设计原则比伸缩性更抽象更通用

1. 简单

使事物尽可能简单,但是不要过于简单。---阿尔伯特.爱因斯坦

软件天然就是错综复杂的。

判断一个东西是不是过于简单的时候,首先要回答的是对谁而言及对什么时候而言。譬如,对你而言还是对客户而言过于简单?是对现在开发而言还是对将来维护而言?

简单是让别的软件工程师以一种最容易的方式使用你的方案

简单是当系统变得更庞大更复杂的时候依然能够被理解

简单不是走捷径,不是为手边的问题找一个最快的方案。

 

重温写过的代码,区分复杂度,以此寻找简单的方案。从自己的错误开始是学习的每一步。

培养敏锐的感觉和能力,从而快速判断出长期而言什么是更简单的方案。

 

如果有机会能够找到一个导师或者能够和擅长发现简单的人一起共事,会进步更快。

简单产品可以从以下4个基本步骤开始

1.1 隐藏复杂与构建抽象

隐藏复杂与构建抽象是实现简化最好的方法之一。

人类的大脑处理能力有限的,要么对系统整体有个了解而不知道细节,要么知道系统的一小部分细节而不了解整体。

保持简单可以让你收放自如地从整体到细节各种粒度地查看系统。

不过,如果系统很庞大,是无法保持整体简单的,你能做的只是保持局部简单。

局部简单的主要方式是确保在设计和实现两个方面上,任何单个的类、模块、应用的设计目标及工作原理都能被快速理解。

  1. 看到一个类时,能够快速理解它的工作原理而无须知道系统其它部分的全部细节。只看着这个类就明白这个类能干什么。

  2. 看一个模块的时候,能不看这个模块本身,而是把这个模块当作一大堆类来看,而每一个类都是可理解的。

  3. 再缩小。当看一个应用的时候,能一眼看出那些关键的模块和它们的主要功能,而无须知道模块里的类的细节。

  4. 看整个系统的时候,能快速看出顶层的应用和它们的职责而无须知道每个应用是如何运行的。

节点表示类,边表示类之间的依赖。

好的设计原则是类之间的依赖关系尽量少

为了实现局部简单,需要将不同的功能分割到不同的模块中,这样当你从一个比较高的抽象层次去观察应用时,无须操作每个模块的职责是什么,只需考虑这些模块是如何交互的就可以了。

模块的层面,可以忽略模块内部的细节,只关注模块间的交换即可。

在庞大又复杂的系统中,当你创建一个独立服务的时候,需要添加一个抽象层。

依赖约定,而不是实现。

服务暴露这个更高层次的抽象,实现这些抽象所承诺的功能,从而隐藏其复杂性。

1.2 避免过度设计

当你试图预测每一个可能的用例和那些极少发生的场景时,

可能会忽视那些最主要的业务场景,

会情不自禁地被那些想象出来的问题牵着鼻子走,不知不觉地就过度设计了,最后会开发出一个比实际需求复杂得多的大而无当的系统。

好的设计方法是可以在后期逐渐添加新的功能特性,而不是一开始就开发一个超级大的系统。

早期先构建一个合理的抽象层次,然后迭代地增加新特性,

要比一开始就设计好全部的功能为将来各种可能进行开发好的多。

开发易于理解的简单系统并能证明其将来是可扩展的才是真正的难题

"这个设计是否可以更简单并且可以在将来依然保持弹性"

每次进行软件设计前都要问自己。

过度设计经常发生在人们以为自己在做正确的事,却选择了错误的方向或者太过关注未来的需求

“这里我需要如何做出权衡”

“这里我是否真的需要”

也建议你和业务相关方多接触,尽量去了解什么是最大的风险及还有什么没有搞清楚。

否则,你会按照那些没有用的教条花费大量的时间去构建一个没人用的系统。

本章的大部分设计原则都要花费一定功夫才能达成,你需要搞清楚合理的复杂和过度设计之间的界限。

因为界限不是那么黑白分明易于寻找,而是需要在一大片灰色地带中自己的斟酌权衡。----所以要做到这一点其实非常难

1.3 尝试测试驱动开发TDD

接受TDD的方法论可以提高简化性。

不必在所有地方都搞TDD-----在一些方法上搞TDD就可以获得一个全新的视角

TDD是一种开发实践:先写测试代码,然后写功能实现代码。

好处:

1.没有单元测试就没有代码,所以也就没有无用的代码。

由于开发先写了测试,就不会写那些没必要的代码,否则就又要去写测试代码。 😙

2.UT可以被当做某种文档,可以展示代码的实现意图、具体用法、期望的执行结果等

TDD可以更好地把握工作的重点

TDD会强制工程师从用户的角度看待问题,有助于开发更清晰更简单的接口。因为是先写UT,所以必须要先想象如何使用你要开发的组件,就不会只想着这个组件的内部实现细节。

这种细微的差别会带来代码设计上的巨大提高和应用程序接口API的简化

要从用户视角思考问题,这样就会更好地开发那些易于用户调用的代码。

别人使用的你开发的接口,那么他想调用的方法是什么?

他想传递的参数是什么?

他期待返回的响应结果是什么?

1.3 从软件设计的简化范例中学习

如果复杂性处理得够好,反而难以体会软件简化的理念。如果事情总是顺其自然,如果可以毫不费力地理解系统,那么也许你遇到的是精雕细琢后的某种简化。认识到你使用的系统是良好设计的系统是一种非常赞的体验。无论何时发现这一点,仔细分析它并寻找其中的模式。Grails、Hadoop、Google Maps API,都是一些简化做得很好的范例,值得认真研究。 1.3.1 Grails是Groovy语言上的一个Web框架。Grails是一个简化如何变得透明的极好的例子。随着研究这个框架的深入并使用,你会意识到这里的每一样东西都被仔细考虑过。你会看到事情问题和期望的一样,扩展功能总是毫不费力。你会意识到这个框架就得更简单是不可想象的。Grails是一件让开发变得容易的杰作。 Grails in Action及Spring Recipes1.3.2 Hadoop 熟悉MapReduce编程范式及Hadoop平台。Hadoop是开源技术领域的杰作,可以处理PB级的数据。Hadoop是一个庞大而复杂的平台,但是它很好隐藏了期复杂的实现。开发发现它解决了多少困难的问题,才让开发者处理几乎无限的数据变得如此简单。如果想对MapReduce和Hadoop有个基本的了解,推荐阅读MapReduce原始论文及Hadoop in Action

1.3.2 Google Maps API一直都以一种格外简单的方式使这些API在解决复杂问题的同时保持足够的弹性。one person->workshop->studio

小结:简单对你的系统的伸缩性有一些潜在的价值。没有简单性,工程师就没法理解代码,不能理解软件,系统就无法保证持续发展。一定要记住:

对于伸缩性,最好是设计简单的系统然后让它好好运行,而不是设计复杂的系统然后突然崩溃

2.低耦合(coupling)【高内聚(cohesion)】:第二个重要的设计原则是保持系统各个部分之间尽量低耦合。

耦合用来度量两个组件之间互相关联及依赖的程度。耦合度越高,依赖强。低耦合是指两个组件之间只有必要的一点点关联和依赖,而没有耦合则是指两个组件之间完全不感知对方的存在。

保持系统低耦合对于系统的健康及伸缩性至关重要,它甚至会影响团队的土气。低耦合和高耦合的不同影响:

  • 高耦合意味着任何一行代码的改动都需要检查系统各个部分是否存在问题。耦合度越高,各种出乎意料的依赖越多,引入BUG的机会越高。譬如,如果对用户认证做了一点改动,然后你想起还必须重构其它五个模块,因为这些模块都依赖了认证的内部实现。

  • 低耦合可以保证复杂性局部化,即复杂只体现在模块内部,不影响整个系统。将系统分解成低耦合的多个部分,可以安排多个工程师分别独立开发各个部分。这样就可以招募更多的工程师扩展你的团队,每个人都可以在不了解整个系统细节的情况下针对局部模块进行开发。

  • 在更高层面上进行解耦合意味着将系统分成多个应用,每个应用都只关注相对较小的一部分功能。这样每个应用就可以按需分别伸缩。有些应用需要更多CPU,而有些应用则需要更多IO吞吐能力或者更多内存。通过将系统解耦成不同部分,可以针对具体应用特点提供相应的硬件配置以达到更好的伸缩性。

2.1促进低耦合。

促进低耦合的最重要的实践是小心地管理你的依赖

这个准则适用于类之间的依赖模块之间的依赖,以及应用之间的依赖系统是全部---它包括了一切:你开发的所有应用和你系统环境中用到的所有软件。应用是系统内部最高层次的抽象,它们提供最高层次的应用服务。你也许会用到五个帐户应用,五个资产管理应用,或者五个文件存储应用。在应用内部会有一个或者多个模块负责实现更精细量多具体的功能特性。就像不同的应用通常是由不同的团队开发和部署,不同模块(比如信用卡处理、PDF展现、FTP接口)也应该对依赖它的开发团队保持足够独立,以便它们可以并行开发。如果你不够信任某个团队开发的某个特定模块,那最好不要让这个模块和你的应用耦合得太紧。最后,模块内部包括了一些类,这是最小的抽象粒度。一个类应该只有一个目的而且代码最好不要超过两三屏。

隐藏细节是减少耦合和促进简化的一个非常好的手段

可以使用public、protected、private关键字促进低耦合。应该尽可能将方法声明为private或protected你的类越少访问其它类,你就越少需要关注这些类的工作原理。因为private方法只在类的内部被调用,所以这些方法可以被更容易地重构和修改,类的复杂性也被局限在类的内部,不用到处查找这些方法在哪里被使用,是否会引起潜在的BUG。暴露太public方法,就会增加外部代码使用它们的机会。一旦一个方法是public的,你就不知道它会在哪里被使用,你就不得不在应用中仔细查找那些调用它的地方。

在写代码的时候,要吝啬一点。在满足需求的情况,尽量少暴露内部信息和功能。暴露的信息越多越早,越增加耦合性,未来需求变更的时候越麻烦。这个法则也适用于各个抽象级别,不论是类、模块,还是应用。

要在较高的抽象层面降低耦合度,你需要让系统不同部分的接触面尽量少。低耦合可以让你重构或者替换系统中的任何部分而不用对其余部分做太多调整。找到解耦与过度设计之间的平衡是个艺术活,工程师经常会忽略抽象的重要性。可以通过设计图来帮助自己做出正确的权衡。当你画应用设计图的时候,连接两个部分之间表示依赖关系的线条的数目决定接触面的大小。

高耦合的应用很难只修改某个模块中的某个部分而不影响另一个模块。此外,模块之间彼此知道对方的内部结构并会直接访问这些部分。

低耦合,模块B的对外开放功能被隔离为一个很小的子集并被显式地声明为public,这样可以降低和外界的接触面,并且让模块有更好的私密性。低耦合的应用中模块间没有循环依赖。2.2 避免不必要的耦合在实践中,某些被广泛使用的方法事实上会增加耦合性。一个典型的导致不必要的耦合的例子是通过public的getter和setter方法暴露类的private成员变量。这种用法源于java Bean的一些早期实践,当时提供getter和setter方法主要是为了适应代码操作工具和IDE。这种实践在Java社区范围内都被完全地误解了。代码和IDE集成这种不良的习惯也会影响到其他的语言和平台。

public的getter和setter会破坏封装性增加耦合性

最好一开始的时候全部方法都是private,然后视情况将那些需要的方法改成protected或public。

关键要记住尽可能多地隐藏而尽可能少地暴露

另一个常见的不必要的耦合的例子是模块或者类的客户端代码必须按照特定的顺序进行方法调用。

有时这些做是有合理原因的,但是更多的时候是源于糟糕的API设计,比如需要调用一个初始化方法。类及模块的客户端使用者应该不需要知道类的设计者期望他们如何使用这些类及模块。

应该可以按照自己的方式任意调用类的public接口

如果你要求你的客户端使用者必须知道API需要以何种方式被调用,那么你就是在增加耦合性。

耦合接触面不但受方法签名影响,也受方法的调用方式影响

处于不同层次的应用、模块、类之间如果出现循环依赖,就意味着在设计的耦合性方面比较糟糕。一般来说,画系统架构设计图很容易暴露循环依赖。一个良好设计的模块架构图,看起来像一棵树(有向无环图),而不像一个社交网络图。2.3 低耦合范式

要想很好地理解低耦合必须经历大量的实践。你像理解简单一样,你可以通过阅读别人的代码,分析别人的系统获取大量的经验。低耦合设计的一个很好的例子是UNIX命令行编程及管道(pipe)的用法。另一个比较好的低耦合的例子是SLF4J。强烈建议了解SLF4J的结构,并且和Log4J及Java Logging API做个对比。SLF4J的角色像是一个非直接层,把log接口调用和实现的复杂性分离开来,显露了一组更简单更清晰更容易理解的API。低耦合是开发弹性软件的最基本的原则之一。建议把设计低耦合的模块放在一个更高的优先级上。

3.不要重复自己(DRY)我认为避免重复是最有价值的原则之一。只做一次,这是极限编程的格式--Martin Fowler重复自己意味着同样一件事你要做好几次。

如果你把同样一件事做了一次又一次,就是在浪费生命。不要把一件事做好几次,而是去做些比较酷的事,比如开发一些新特性或为客户思考一些更好的解决方案。试着说服你所在的团队或者你的老板采取一些基本措施以避免重复----比如,如果有重复的代码那么代码审查不通过,每个新写的类都必须先写单元测试。

让开发工程师浪费时间做重复的事有很多原因:

  • 采用低效的过程:在软件开发的各个阶段都会发生,设计新功能、交付、开会等,通过对这些地方进行持续改进可以获得良好的收益。获得反馈、持续改进、然后重复这个过程。很多时候,团队意识到自己在浪费时间却无可奈何。当你听到“我们就是这么干的”或者“我们一直都这么干”时,常常意味着这里有低效的过程并有改进的机会。

  • 缺乏自动化当手工部署、编译、测试、打包、配置开发机器、准备服务器、为API写文档的时候,-----这就是在浪费时间。巨大的浪费就是建立在这种不断增加的微小的工作量之上。从第一天开始就尝试将编译部署的工作自动化,当然你也需要一直维护这些自动化的工具。

  • 非得自己发明,也就是常说的重复发明轮子这个问题的表现症状是:不复用已有的代码而非要自己写代码。年轻的工程师,乐于重写那些很容易就能用到的东西(对公司内部或者开源世界而言),比如实现hash功能、排序、b树、MVC框架或者数据库抽象层。虽然这种重复不是字面意思上的重复,但这依然是在浪费时间。因为完全可以使用别人已经开发好的工具和代码库。任何时候,想写一个通用类库的时候,都先上网搜一下,通常已经有很多实现得很好的开源代码了。

  • 复制粘贴代码为了节约时间,把这索代码复制粘贴过来,然后只对其中一小部分做了一点改动。祝贺你,你现在有两份相似的代码需要维护了。总会有某个时候,你会意识到你对这部分代码做的任何改动都不得不在其他地方再改一次,总会有一些已经修复有BUG会再一次出现,因为你忘了修改粘贴过来的那部分代码了。试着让团队建立一些规则:比如“我们永远不会复制粘贴代码”。同时给每个人权力在代码评审时指出重复的代码,让每个人彼此之间都感受到一些良性的压力。

  • “这个代码不会用第二次所以就凑合一下”的态度有时,这些问题会再一次出现,你就不得不再用一次这些当初被当作一次性的随便搞搞的代码。那么问题来了,这些代码即没有文档,又没有UT,也没有合适的设计,很难用。更糟糕的是,其它工程师可能会复制粘贴这些随便搞搞的代码用在他自己的一次性脚本上,悲剧再一次上演。

复制粘贴代码

我相信复制粘贴代码是一个常见的问题。出现这种情况主要是因为R&D通常没有意识到自己写的代码越多,维护的成本和代价越大。复制粘贴导致应用的代码变得更多,更多的代码导致维护的成本更高,随着各种技术问题的堆积,这种成本会呈指数级上升。应用中存在重复代码,一个地方修改就需要所有重复的地方都修改,不要追踪复制的代码之间的差异,对所有复制粘贴的代码做回归测试。由于复杂度随代码行数成指数级增长,所以复制粘贴的代码极其昂贵。事实上,复制粘贴代码是一个非常严重的问题。处理代码复制粘贴可能是一件让人很头痛的事情,不过没有什么事是重构不能搞定的。一个好的开端是在代码库中搜索每一个重复的功能。一旦你这么做了,你就可以更好地了解哪些组件受到影响并且该如何重构它们,可以考虑创建一个抽象类或者将重复代码抽取到一个独立的更通用的组件里。

在对付重复代码的战斗中,组合或者继承都是你的朋友

另一个对付复制粘贴代码的好办法是使用设计模式和共享类库设计模式是解决一类通用问题的抽象方法,是软件设计层面的解决方案,可以应用到各种系统各种领域中。设计模式需要考虑如何组织OO代码、依赖、交互,但是不需要考虑具体的业务问题。设计模式会建议如何在模块中组织代码,但是不会指出需要使用算法或者业务特性及如何动作。也可以在更高层面上通过Web服务去对付代码复制。不要在每个应用中开发相同的功能,而是创建一个服务,然后在整个公司范围内复用这个服务。

阻止功能复制的一个办法是让功能使用变得最简单。 例如,你的类库提供了20个功能,80%的时间被使用的都只是其中的5个功能,那么保持这5个功能尽可能地容易使用。

容易使用的东西更容易被复用

如果你的类库是可以最容易完成一件事情,那么每个人都乐意使用你的类库。如果使用你的类库很困难,那么大家就会想重复再搞一个。

4.基于约定编程,是解耦的主要方式。

基于约定编程,或者说基于接口编程,是另一个重要的原则

基于约定编程是将客户端代码和功能提供代码解耦的主要方式。客户端仅仅看到这些约定并只依赖这些约定编程。系统不同部分解耦并将变化隔离开,这会对开发带来很多好处

约定是一组功能提供者承诺实现的规则

客户端代码可以理解这些规则并依赖它们。它们描述了哪些软件提供了哪些功能,但是不需要客户端代码知道这些功能是如何实现的

“约定”这个词在不同场合代表不同意思。

  • 当讨论OOP的成员方法时,约定是指方法的签名,定义了期望的输入参数和期望的返回结果。约定不会规定结果该如何计算出来,这是实现的细节,使用无须关心这些。

  • 当讨论类时,约定是指类的public接口,包含了所有可访问的方法和签名。

  • 到抽象层,模块的约定包含了所有的公开可见的类/接口及它们的方法签名。

  • 对整个应用而言,约定通常意味着一些描述Web服务API的表格

抽象层次越高,约定的复杂度和范围越广

约定有助于将客户端和功能提供者代码解耦。只需要保持按约定开发,客户端和功能提供者可以各自独立修改。这会让代码更隔离更简单。在设计代码的时候,尽可能创建明确的约定,也尽可能地依赖约定(而不是实现)进行开发。每个提供者都是独立的模块,由于它们都履行相同的约定,客户端可以使用其中任何一个而无须直接知道究竟使用的是哪一个。在客户端中,任何完成相同约定的代码都是同等的,这样进行重构、UT、修改实现都变得非常容易。

想想让基于约定的编程更简单,可以考虑把约定当作真实的法律文件。当人们同意按法律文件做事的时候,他们会变得对细节更敏感,因为如果某个条款没做到的话,他们就要承担相应的责任。在软件设计中也类似,低耦合的约定的每个部分都是在增加未来的责任作为一个功能提供者,太多不必要的东西就是在增加将来的负担因为任何时候如果你想做出改变,你就不得不和所有的客户对改变的约定进行谈判(这个改变会在整个系统中蔓延)

设计类的时候,首先考虑的是客户端真正需要的功能是什么,然后设计一个最小的接口当作约定最后实现代码以完成约定。针对类库和Web服务也遵循同样的方式,当你暴露一个Web服务API的时候,应该是明确的并要仔细考虑究竟暴露给客户端的是什么。需要的时候,应该做到容易增加新特性及发布更多数据,但是开始的时候应该尽可能地实现一个简单的约定。HTTP是基于约定编程的范例。HTTP被不同系统使用不同的语言在不同平台中实现,然后,HTTP是最流行的协议之一。有些HTTP协议约定的客户端是Web Browser,比如Firefox、Chrome。它们被不同的组织以不同方式实现,按不同的进度更新和发布。HTTP提供者,主要是Apache,IIS,Tomcat这样的Web服务器,代码也被不同组织用不同的技术实现,并且被独立地部署在全世界的各个地方。更棒的是,还有很多人们不知道的其它技术实现的HTTP约定,比如Web缓存服务器Varnish和Squid,同时实现了客户端和提供者。客户端和提供者通过HTTP协议松耦合。尽管由于生态环境的复杂性,所有的应用都被互联网联系在一起,但HTTP还是体现出实现上的弹性及提供者透明的可替代性。HTTP是通过约定进行解耦合的一个漂亮的例子,所有应用都有一个共同点,它们实现或依赖一个相同的约定。

4.画架构图 “你知道真正的架构是什么吗?是一门画线的艺术。画一条线连接依赖并指明依赖的方向”--Robert C.Martin

对于架构师和技术领导者而言,画架构图是一项必备的技能。

一张架构图可以表述很多信息,所谓一图胜千言。通过架构图,可以将系统设计文档化,跟其他人分离信息,也可以帮助自己更好地了解自己的设计。特别是适应了敏捷开发实践,学了很多创业方法学以后,不会花太多时间做前期设计,但是这并不意味着一直都不需要。

如果你觉得画架构图挺难的,你可以尝试先画那些已经开发好的系统的架构图。你自己开发过的系统画架构图更容易一点,因为你毕竟更了解。等你熟悉了各种架构图的画法,再开始前期设计架构图。从使用者的视角观察类接口的设计并进一步充实它们,试着为接口写单元测试,然后画一些简单的架构草图。通过假想用户视角及画简单架构图,你可以在写代码之前验证自己的设计并发现其中的缺陷。等你熟悉了画各种架构图以后,尽量多做前期设计。如果你发现前期设计很难做的话,也不要沮丧,从先写代码变成先做设计本来就不是一件容易的事,所以请做好准备花几个月甚至几年的时间去熟悉这个过程。

假设你需要设计一个断路器组件。断路器是一种设计模式,用于增强系统健壮性,保护系统不受其它组件(或者第三方系统)失败的影响。断路器在你的代码执行某个操作之前先去检查其可用性。

  • 开始的时候先写主要接口的代码草稿,

  • 然后写使用者代码草稿验证接口。使用者代码可以是单元测试,也可以是一些无须编译的代码草稿。这个阶段仅仅是确认接口设计是明晰的易于使用的。

  • 然后你就可以画一个时序图展示使用者如何和断路器进行交互,以此进一步确认设计的正确性。

  • 最后,再画一个类图的草图,检查是否违反某些设计原则,结构是否简单清晰。

我相信遵循前面讨论的这些简单的设计过程就可以在创业公司做好前期设计工作了。你可以从多个角度审视自己的设计,并会因此获益良好。同时,你也可以降低开发风险,不至于做了一个不靠谱的设计,等发现的时候就已经掉坑里了。在写设计文档或者了解大规模系统的时候,有三种架构图特别有用:

  • 用例图

  • 类图

  • 模块图

随着你的公司规模越大,你的团队人越多,你越会从这三种架构图中获益。

4.1.用例图,是一种比较的图

定义了系统的用户是谁,他们可以执行哪些操作

用例图不考虑技术方案,仅仅关注业务需求,是一种将功能特性和业务需求提炼出来的好办法。当你要做一个新功能的时候,先画一个简单的用例图。用例图包含用一个小人图标表示的角色、角色可以执行的操作,以及各个操作之间的关系结构。用例图也可以呈现与外部系统的交互,比如远程Web服务API或者任务调度器。用例图不需要包含太多需求细节,做到简单、清晰、易于阅读就好的。画用例图的时候尽量在一个较高的层次上提炼关键操作和业务过程,而不是陷入到海量的细节中去。ATM应用的用例图虽然ATM机包含大量的关于认证、安全、事务处理方面的细节,但是用例图只需要关心ATM完成用户的哪些操作就可以了。从这个角度看,按钮在屏幕上怎么排列或者ATM怎么去实现每个功能,这些都不重要。你只要关注一个需求的概要,就可以了解ATM系统的主要意图并定义各种约定。

4.2.类图

类图展现独立模块的结构

一个典型的类图包含接口、类、关键方法名,以及它们的关系。由于每个类都用一个节点表示,每个依赖都用一条线表示,所以从类图上很容易看出耦合关系,可以看出哪些类耦合度更高,那些类更独立。简单看每个节点上连接的线的数目就可以判断出这个节点包含的依赖有多少。类图是一种非常好的可视化手段,很好地展现了类、接口及其交互关系。简化的E-mail模块类图 ,这里的关键元素是类、接口、最重要的方法及依赖关系,不同的依赖关系用不同类型的线条表示。在这个例子中,EmailService有两个实现类,一个使用SMTP协议直接发送E-mail,另一个将E-mail加入队列延迟发送。EmailService也是一个基于约定编程的例子,任何依赖EmailService的程序都可以使用SMTP或者队列发送E-mail,却无须知道E-mail最终是使用哪个实现类发送的。接口只能依赖其它接口而不能依赖具体类that's to say ,要尽可能地依赖接口编程。

4.3.模块图

模块图很像类图,也是描述结构和依赖关系的,

模块图和类图的不同在于模块图关注的抽象层次更高。

模块图可以认为是代码层面经缩略图,是类图的上面一层架构图模块不同关注类和接口,而是关注系统更大粒度的组成部分,展示模块之间的依赖和交互。模块可以是一个包,也可以是逻辑上实现特定功能的组件。eg:一个支付服务(PaymentService)模块图的例子,展示了一个实现支付功能的应用中支付服务和其他部分之间的依赖关系。模块图通常关注应用中的特定功能有关的部分,而忽略那些不想干的部分。由于系统将来会变得更大,所以最好一开始就画多个模块图,每个模块图只关注一个特定的功能,而不是把所有的功能都画在一个模块图中。理想情况下,每个模块图都应该足够简单,你能全部记住并在脑子里重画一次。

保持创新而不是担心自己画图画的对不对

实践中,架构图容易理解比画得漂亮及符合规范标准更重要

建模语言UML及设计模式推荐使用ArgoUML作为桌面UML绘图工具,这是一个开源的Java应用程序,可以在整个公司内协作使用,无须 将软件设计上传到云上。如果你喜欢基于云的方案进行在线协作设计,试试draw.io,这是一个集成了Google Drive的免费且容易使用的在线服务。 draw.io是我的最爱,本书里几乎所有的架构图都是用draw.io画的。5.单一职责

单一职责是降低代码复杂度的一个有效手段

单一职责原则是说你的类应该只有一个职责而不宜更多。单一职责可以降低耦合,增加简单性,易于重构,代码复用及单元测试----这些 目前为止,讨论过的所有的核心原则。遵循这个原则设计出来的类更简单更小,因此易于重构和复用。有时候就眼前来说,也许无视新功能是否为某个类的职责,直接往一个类里简单地添加方法更容易然而,这样做几个月后,你的类会变得非常庞大和其他类的耦合也更紧密。你会看到类之间出现的各种交互操作并不像你期望的那样,这些类会进行一些无关的操作。同时,由于类变得很庞大,因此几乎很难完全理解它的行为和角色随着时间的推移,这种情况会变得越来越严重,几乎每行代码都会带来复杂度的巨大增加。5.1改善单一职责事实上,并不存在一个必须遵守的法则去衡量你的类是否符合单一职责原则。不过还是有一些最佳实践帮助你做出判断。

  • 一个类的代码少于2-4屏

  • 确保一个类的依赖不超过5个接口/类

  • 确保一个类有一个明确的目标

  • 用一句话总结一个类的职责,并把这句话放在类名上面作为类的注释。如果你发现很难用一句话总结一个类的职责,那有可能这个类的职责并不单一

如果你的类不能满足这些最佳实践,那你可能需要好好看看这个类并且准备重构。更高的抽象层面,你需要将功能按模块分隔并避免功能重叠。具体做法可以参照类的设计------试着用一句话总结模块或者应用的功能,不过要从更高层次总结。例如,你可以说:“文件存储系统允许用户上传文件、管理文件并支持复杂的文件搜索”,这样应用目标看起来就很清晰。可以使用一个明确的接口(比如一个Web服务定义)限制模块职责范围并将其从其它部分隔离开来。

5.2单一职责的例子我们看一个E-mail地址验证的例子。如果你把验证逻辑直接放在创建用户帐号的代码里。就没有办法在其他地方复用了。你就不得不复制粘贴这些代码或者在这些本来没什么关联的类之间进行一些让人难受的依赖总之,你会破坏软件设计的核心原则。把验证逻辑剥离出来,你就可以保证其只有一个实现并能在其它复用过了一段时间, 你如果需要对验证逻辑进行修改,比如需要在域名中支持UTF-8编码,只需要重构这个类就可以了。-------------单一职责, 单独变化设计一个类专门负责E-mail验证比将这部分代码到处复制更简单,更容易将变化隔离开来。单一职责原则的另一个作用是你会变得更乐于写可测试的代码因为类的内容变得更少逻辑更少依赖,所以更容易进行独立测试进一步掌握单一职责的办法是研究策略迭代器代理适配器模式。了解领域驱动设计及更好的软件设计

6.开闭原则

好的架构就是可以让你延后做出决定----Robert C.Marti

开闭原则是指,当需求变更或者增加新功能时,你不需要修改现有的代码。开闭的意思是:

向扩展开放,向修改关闭。

如果我们设计的代码在将来扩展新功能的时候无须修改,那么我们就遵循了开闭原则。正如Robert C.Martin所言,开闭原则可以让我们保留做出选择的余地,延迟到将来做出某些细节决定,这样就减少了现有代码的需求变更。这个原则的目的是增加软件的弹性,使将来变更的代价 最小化eg.考虑一个排序算法,需要对一个记录雇员对象的数组基于姓名进行排序一般实现中,你可以在一个类里包含算法及其他全部必要的代码,假设类就叫EmployeeArraySorter。你可以在这个类里只暴露一个方法,允许对雇员对象数组进行排序,然后就可以说这个功能开发完成了。虽然这样是能解决问题,但是显然不够有弹性,事实上,这是一个非常僵硬的实现。由于所有的代码都在一个类里,你几乎不能在不修改代码的情况下扩展或者增加任何功能。假如你有一个新的需求,需要对城市基于人口进行排序,就不能复用这些已经开发好的代码。这时,你就不得不面临一个尴尬的选择,要么扩展EmployeeArraySorter做一些和原先设计完全无关的事,要么复制粘贴这个类的代码然后再修改。幸运的是,你还有第3个选择,就是重构这个类使其符合开闭原则。开闭原则需要你将这个排序问题分解成几个更小的任务 ---社会分工的进一步细化??第个任务都就应该是独立的,不影响其余部分的复用。你可以设计一个接口Comparator用来比较两个对象,再设计一个接口Sorter用来执行排序算法。Sorter会利用Comparator对输入的数组进行排序。例如,你想改变排序对象,只需要增加一个新的Comparator实现即可,无须对现有代码做任何修改。如果你想改变排序算法,也不需要修改Comparator或者Sorter,只要重新创建一个Sorter接口的实现即可。各个不同部分独立变化,可以减少变化影响的范围,也可以扩展现有的代码而无须对它们做任何改动。另一个开闭原则的好例子是各种MVC框架这些框架由于其简单和易扩展的特性而广泛应用于Web开发。回想一下,你什么时候需要修改MVC框架的某个组件? 如果这个MVC框架的架构设计得足够好,答案是永不需要。不过,虽然不需要修改,但你还是可以扩展MVC中的某个组件,增加新的路由、拦截请求、返回不同的响应、覆盖缺省的行为。不必修改现有的框架代码,就能扩展其功能,这就是开闭原则所起的作用。​ 和掌握其他设计原则一样,学习开闭原则,开始的时候先熟悉了解各种框架。观察开闭原则的各种实现思路,领会开闭原则的实现模式,例如Java语言实现的SpringMVC框架就是一个很好的开闭原则的例子。用户可以弹性地实现各种功能而无须修改框架本身,而客户端代码跟框架也几乎没有多少耦合。通过使用annotation和基于约定编程,你开发的绝大部分类甚至都不知道Spring框架的存在!7.依赖注入依赖注入是一种降低耦合 改善开闭特性的简单技术。依赖注入对类需要依赖的对象提供一个引用实例,而无须类自己创建依赖的对象。

依赖注入的核心思想是知道得越少越好。

尽可能地让类不要知道如何去创建需要的依赖,以及这些依赖来自哪里,是如何实现的。这看起来只是从“拉取”变成“推送”的一个微小变化,但事实上会对软件设计的弹性产生巨大影响。我们来看一个CD播放器的依赖注入的例子。所有的CD播放器都持有一个CD接口,知道如何去读取音轨,如何对音乐进行解码,读取CD内容的Laser device的光学参数是什么。插入到CD播放器的CD是一个依赖,没有CD,CD播放器就没法正常工作。发现依赖职责交给用户,CD播放器就可以保持简单。从而使CD播放器的利用性更强,它不必知道burn到CD上的每一个标题,也不必知道专辑上的每个组合,只需要知道CD的各种可能形式及其组合形式就可以了。CD播放器需要你(用户)去提供一个可读的CD,一旦你提供了一个CD实例,CD播放器就可以正常工作了。此外,还有一个附带的好处就是CD播放器允许插入非标准的CD。你可以插入一张空白盘(NothingToDo对象的实例),或者一张音轨畸变的测试盘,这样你就可以做异常测试了。eg:通过硬编码的方式记录各种CD,如果你想播放一张新CD,就不得不修改它的代码。-----一个负担过重的CD播放器。再看一下使用依赖注入方式实现的CD播放器,不必知道任何CD自身的信息,只需要提供一个CD的实例就可以。使用这种方式,你就可以实现对新增开放(播放新的CD)而对修改关闭(CD播放器永不需要修改),这里面还有基于接口的约定,CD的编码方式要和CD播放器的解码想匹配。

实践中,依赖注入通常认为类中不要使用new这个关键字,

需要依赖的对象实例由调用者提供

如果使用得当,依赖注入可以有效降低类的局部复杂度,使其更简单。无须知道谁来提供依赖的实例,也无须知道实例从何而来,这样类就可以聚焦自己的主要职责类的代码更简单,不需要太了解系统的其他部分,不太需要变化和单元测试。将集成的代码从类中移除,可以使类更加独立、可复用、可测试。依赖注入在面向对象编程OOP社区已经有多年实践。实现依赖注入不需要任何框架。学习依赖注入推荐Spring框架或者Grails框架,它们是依赖注入方面非常好的例子。

8.控制反转IOC

依赖注入是非常重要的一个原则,也是范围更广的控制反转原则的一个子集。

依赖反转主要解决对象的创建及依赖的组合,而

控制反转则更通用,可以在各个抽象层面解决各种问题。

以前面向过程,一个方法写到底,相关的外面依赖都需要自己初始化,核心业务逻辑和外面依赖的初始化及管理都堆积在一起IoC 反转是对这种情况的改变,相关依赖通过DI的方法注入,外面依赖的控制交于容器或第三方

控制反转(IOC)是一种从类中移除职责的方法,从而使类更简单,与系统其它部分更少耦合。控制反转的核心是不必知道是谁、怎样、何时创建和使用对象,尽可能地简单易于理解,对于软件设计而言,各部分互相知道越少越好

IOC在一些框架中重度使用,包括Spring、Symfony、Rails,甚至JavaEE容器。你不必创建类的初始化方法,这些统统交给框架来做。IOC框架接收Web请求创建对应处理类的实例交给对应的模块处理。IOC也被称为好莱坞原则,好莱坞原则Hollywood principle是说“don‘t call us, we‘ll call you“运用到实践中,就是你的类实例不必知道自己什么时候 被创建被谁使用,自己的依赖又是如何被创建的。你的类是一个插件外部力量决定这些类什么时候被创建如何被使用想象一下只用纯Java不需要任何框架开发一整个Web应用。没有Web服务器,没有框架,没有API,只有Java。为了完成这样一个复杂的应用,你需要自己写非常多的代码即使你决定使用一些第三方的库,你也必须自己控制整个应用的流程通过使用框架,你可以减少代码的复杂度不止是可以减少需要写的代码的数量,还可以减少开发者需要关注的事情需要做的只是让框架回调你的代码框架会创建类的实例请求到达的时候,框架会调用你的方法进行处理,并控制执行的流程从一个扩展点另一个扩展点eg: 一个用Java(没有用任何框架)编写的Web应用。这个例子中,大量的代码模块用于和外界进行通信。应用需要自己打开网络端口,写日志文件,连接外部系统,管理线程,解析消息。应用需要控制所有的事情,这意味着你需要考虑太多与业务无关的事。如果你使用IOC框架,框架处理了大量的非业务事务,应用甚至不需要关心这些事情的存在。虽然系统还是要处理这些事情,但是都和应用无关不会改变整个系统的复杂性,但是会极大地降低应用的复杂性IOC是一个很通用的概念。你可以为任何类型的应用创建各种控制反转的框架,不只是MVC框架或者Web请求处理框架。

视频这些东西,超过半分钟,10秒或20秒,他觉得没兴趣,他就换了。 执行困难

短是一个感觉,觉得这个时间,有没有讲透

公众的节目,众口难调

记得第一道菜的样子

一个好的IOC框架包含如下特性:

  • 可以为框架开发插件

  • 插件是独立的,可以被随时插入移除

  • 框架可以自动检测到插件,或者通过配置文件加载插件

  • 框架为每一种插件类型定义接口,而不会与具体插件耦合

基于IOC框架写代码就像把钱放在鱼缸里。鱼缸里可以放很多鱼,它们互不干扰,但是它们生活在一个它们不能控制的世界里。你决定鱼缸的环境是什么样的,什么时候喂鱼。你就是一个ICO框架你的鱼就是插件,它们生活在一个被保护的自己却不知道的泡泡里。

9.为伸缩而设计为伸缩而设计是一个有难度的艺术活,本书中描述的每一项技术都要付出相应的成本。作为一名工程师,你需要在没完没了的伸缩和每一种对应的方案实践之间进行仔细的权衡。不要为了那些永远也用不着的伸缩性需求进行过度设计,要认真评估系统最可能的实际伸缩性需求进行相应的设计。

从业界情况看,很多创业公司完全等不到做任何系统伸缩就倒闭了(这样的公司大概占90%)。剩下的大部分公司也只需要较少的伸缩性就够(这样的公司占了大概9%)。只有极少数的公司会持续成长,需要进行水平伸缩(只占1%左右)。

目前为止,我们讨论的各种设计原则都是为解决复杂性耦合性,同时,这些原则大部分也有助于改善系统伸缩性。随着你对伸缩性的深入了解,能深刻意识到伸缩性方案都可以浓缩成三个基本设计方法。

  • 增加副本:同一组件重复部署到多台机器

  • 功能分割:基于功能系统分割成更小的子系统

  • 数据分片:在每台机器上都只部署一部分数据

这些方法会提供某一方面的好处,同时也需要付出另一方面的成本9.1 增加副本如果你从头开始构建一个系统,最简单最常见的伸缩性策略是增加副本。副本指的是组件或者服务器副本。任何时候两个副本都是可以互换的,任何请求任何副本上处理都是一样的。换句话说,发送请求到任何一个副本响应都是一样的。通过增加副本实现伸缩时,需要关注应用状态如何存储及副本之间如何同步状态变更。所以这种方式最适用于无状态的服务,无须同步状态。如果应用是无状态的,那么对于请求而言,一台新服务器和一台已经存在的服务器没有区别。

无状态服务是指一个服务不会依赖本地状态,处理一个请求也不会影响到服务自身的行为方式。要获得正确的结果无须特定的服务器实例。

增加副本实现伸缩不是无状态服务的专利。事实上,数据库使用这种方式通过副本进行伸缩已经多年。通过增加副本进行伸缩就像医院里的急救室。如果你预算充足,可以雇用足够多的职业医生并且购买足够多的手术室与设备,就可以很容易地让很多病人得到救治。医生和病房的数量一样,能救治的病人数目一样。对于Web层而言,增加副本是最容易、成本最低的伸缩方案。增加副本实现伸缩的主要挑战是对于有状态的服务难以用这种方式伸缩,需要找个办法同步状态以实现副本任意可交换9.2 功能分割第二个重要的伸缩性策略是功能分割,适用于各个地方 各个层面这个方案的核心是发现系统的各个功能部分,然后将它们创建独立的子系统。当我们讨论基础设施架构的时候,功能分割批的是物理服务器分离部署。将数据中心的服务器根据类型进行划分,有对象缓存服务器、消息队列服务器、队列工作者服务器、Web服务器、数据存储服务器、负载均衡器等。这里每一个组件都可以部署到主应用服务器上,但是这么多年来,工程师们逐渐到更好的办法是将这些功能组件部署到独立的服务器上。功能分割的另一种更先进的方式是:将系统切分成一些自给自足的应用主要适用于Web服务层,是面向服务架构(SOA)的一种主要实践。回到eBay竞价应用这个例子,如果你有一个Web服务器层,就可以构建一系统的松耦合的Web服务处理实现各种功能。这些服务可以拥有自己的逻辑资源,诸如数据存储、队列、缓存等。一个功能分割的场景,两个独立的功能:资料服务和调度服务。这些服务可以共享底层基础设施,也可以拥有独立部署的底层基础设施,比如数据存储等。服务更多的自主性,一方面可以更好地基于约定编程,另一方面也可以让服务自己决定需要什么样的组件如何对自己进行伸缩功能分割常用于底层:将应用分割成多个模块,然后将不同类型的软件部署在不同的服务器上(比如,数据库和Web服务部署在不同服务器上)。在大一点的公司,也会对顶层进行功能分割,即创建多个独立的Web服务。这种情况下,需要将单个应用切分成多个小一点功能服务这样做还有一个好处是多个团队可以基于各自的代码库 独立开发,不同的服务也可以根据各自的伸缩性需求独立伸缩。功能分割会带来很多好处,但是也有缺点。分割后的功能,最开始时会带来很多管理方面的困难功能分割可以分割的数量 有上限,限制了对功能分割这种伸缩性方案的使用。你不能无限地使用这种方式对应用无休止地切分,以使Web服务变得更小。

9.3数据分片第三种主要的伸缩性策略是将数据分片,然后将不同的分片数据保存在不同的机器上,以保证每台服务器上存储的数据量都不会太大。这种方式也被称为无共享架构原则每台服务器都拥有自己的一个数据子集,彼此之间完全独立每个节点完全自治这样,每个节点都可以独立变更自己控制的部分数据,而无须在各个节点之间复制状态变更信息。无共享状态意味着没有数据需要同步,不需要锁,失败也会被隔离,因为节点之间没有依赖​ 再来看eBay竞价应用。回顾一下,首先我们通过增加服务器(增加备份)实现伸缩,然后将Web服务切分成两个 独立的服务,这样我们就获得了两种不同的伸缩方式此外,还有一种伸缩方式:对数据负载分布式伸缩。这种方式的一个例子是:对Web服务的对象缓存中的数据进行切分​ 要想让对象缓存实现伸缩,一个办法是增加副本,但是这就需要在所有的服务器之间同步状态另一个办法是功能分割,可以将Web服务器响应缓存在一台缓存服务器,而数据查询结果缓存在另一台服务器上。不过这两种方法都不怎么样,都没有办法让我们的伸缩持续下去。

一种更好的办法是在缓存对象的Key缓存服务器之间创建一种映射关系。

根据缓存对象的首字母将缓存对象分布到不同的服务器上。实践中,应用会使用更复杂的方式进行数据分片,但基本根据是一样的。每个服务器都得到一个数据子集,并只需管理这个子集即可。由于每个服务器都只存储较少的数据,所以服务器可以把更多数据存储在内存中并得到更快的响应速度。将来如果需要更多存储能力,只需要增加服务器修改映射关系即可。还是用医院做类比。如果医院提供预约就诊服务,就可以用数据分片的方式对专家(数据)进行伸缩。不能让病人每次预约到的都是不同的专家,解决这个问题的一个办法是将医生的名字写到每个病人的病例卡中。病人来挂号时,就把分配给对应的医生,这样客服人员可以很容易地根据预约帮病人找到医生。即将数据请求需要的数据--->某个缓存服务器 是固定的,每个缓存服务器只需要存储部分数据即可数据分片配合增加备份,可以实现几乎无限的伸缩性。只要能对数据进行正确的切分,就能增加更多的用户,处理更多的并发连接,收集更多的数据,将系统部署在更多的服务器上。不幸的是,数据分片几乎是最复杂*代价最昂贵的技术。数据分片最大的挑战是在进行数据访问前,必须先找到存储需要的数据所在的分区的服务器,而如果一个查询涉及多个数据分区,那么实现就变得异常低效和困难。

10.自愈设计

一个足够大的系统一定处于某种部分失效的连续状态之中---Justin Sheehy

设计软件一定要考虑高可用和系统自愈能力。

一个系统被认为是可用的,就是说从用户角度它一直按预期运行,完成其功能,而并不关心其内部是否存在某种失效状态,只要不影响用户使用就可以。按句话说,你需要让你的系统表现出所有组件都在正常运行,即使出现某些设备宕机或者进行发布维护也是如此。

一个高可用的系统对于其它用户而言,任何时候都是可用的。对于高可用没有一个绝对的衡量准则,不同的系统有不同的高可用需求。系统的可用性一般不会用一个绝对值直接度量,而是用“几个9”来衡量。我们说一个系统两个9,是说这个系统99%的时间可用,也就是每年大概有3.5天不可用(365天*0.01=3.65天)。相对地,一个系统有6个9的可用性,是说这个系统99.999%的时间可用每年不可用时间5分钟

可以想象,不同的业务场景有不同的可用性要求。要记住的是,系统越大,系统失效的概率越高如果你需要连接5个Web服务,每个服务连接3个数据存储,那么你就要依赖15组件。任何一个组件失效,整个系统都会不可用,除非你能优雅地控制失效进行透明地失效转移当你进行伸缩扩容的时候,失效也会变得更加频繁。你有1000台服务器,那么每天都可能有几台服务器宕掉。糟糕的事情可能远不止于此,很多原因都会导致失效,比如停电断网,以及人为失误。设计一个伸缩性架构必须把各种失效状态当作常态,而不是特殊情况对待。如果想设计一个高可用的系统,就必须做最坏的准备才能得到最好的期待。要不停地想哪里会出错,出错后该怎么办。

  • 保持系统高可用是一种态度,将这种态度发挥到极致的一个例子是Netflix开发的一个叫做Chaos Monkey(捣蛋鬼)的系统。Netflix可用性工程师认为证明一个系统真的高可用的方式是,真的去制造一些事故,然后看这个系统怎么处理。Chaos Monkey就是这样一个服务,它会在上班时间随机关掉一些 Netflix的基础设施组件。这看起来很荒谬是不是?公司可能都会因此而挂掉。但是最后真的证明了他们的系统可以处理任何类型的失效。

  • 另一种类似的高可用态度是一个叫做Crash-Only(只需宕机)的概念。Crash-Only的鼓吹者认为系统应该随时为宕机做好准备,系统任何时候重启,都无须人工干预。这意味着系统是否正在处理请求,处理队列消息还是做任何类型的工作,都能够自己检测到自己的失败修复必要的错误数据、重启,然后像正常时一样工作。CouchDB是一个很流行的开源数据库,它就是遵循这个原则开发的,这个系统不提供任何的关闭功能。如果想关闭一个CouchDB实例,只需要终止进程即可。

如何才能证明你的系统能够处理各种失效状况?我曾经见证过很多宕机事件,有的因为本地服务器存储了状态,有的因为不能正确处理网络延迟持续地 测试各种失效场景是提高系统可用性的重要方式。实践中,确保系统高可用的主要手段是消除失效单点的风险和优雅地进行失效转移。

失效单点是指基础设施中任何一个部分对系统的正常工作都是必要的失效单点的一个可能的场景是域名系统(DNS)服务器,如果你只有一个域名服务器的话。另一个可能的场景是数据库主服务器或者文件存储服务器。

识别失效单点的一个简单方法是画出数据中心架构图每个设备(路由器、服务器、交换机等)都画上去,然后问自己,当某个设备宕机的时候会发生什么。当识别出失效单点以后,就和业务团队一起讨论对其做冗余是划算。有时,冗余很便宜很简单,有时又很昂贵很复杂,这需要仔细权衡。冗余是指数据或组件有至少两个备份当其中一个备份失效了,系统可以使用另一个备份服务用户。一个缺乏冗余的系统需要特别关注,这方面的最佳实践是准备一个灾难恢复计划(有时候也叫业务持续计划),记录对系统各种灾难性问题如何进行恢复最后,如果你想要一个高可用的完全耐受各种失效的系统,也许该实现系统的自愈功能。自愈比优雅地处理失效更进一步,它能够检测并自动修复问题,无须人工干预能自愈的系统是Web应用的圣杯,不过想打造这样一个系统也是非常困难且代价高昂的,比嘴上说说不知道要难多少倍。这里给一个自愈的例子---开源数据库Cassandra处理失效的办法。Cassandra中系统可以透明地处理数据节点的失效。一旦集群发现某个节点失效,就立刻停止发送新的请求给失效的节点。对于客户端而言,失效的时间就是发现失效节点需要的时间。一旦失效节点被放到黑名单里,客户端就可以正常读写数据,集群中其余的节点会对失效的节点提供数据冗余。当失效节点恢复上线后,会带着丢失的数据运行,好像系统什么都没有发生一样相同地,如果用一个新的空白服务器节点替换一个失效的节点,也不需要系统管理员像传统关系数据库那样,必须从备份节点恢复数据。增加一个新的空白数据节点会引起Cassandra集群的数据同步,过一段时间,新加入的机器就填满了数据。如果一个系统能够检测到自身 存在部分失效或者潜在的不可用,并且能尽快进行自我恢复,这个系统就是自愈的。

系统能在最短时间恢复,并且能够自动化地完成这个过程,这就是关于自愈的一切。

平均恢复时间是可用性方程的一个重要组成部分。你越快检测到,去处理,进行修复,可用性就越高。可用性的真实计算公式是:平均失效时间/(平均失效时间+平均恢复时间)减少平均恢复时间,就可以增加可用性,即使失效次数没法控制的情况下也是如此。当使用AWS Web服务这类云主机服务时,由于云服务商使用廉价硬件,他们会在低失效比率和低价之间权衡。这种情况下,你没法控制平均失效时间,所以只能更多地关注平均恢复时间。

小结无论你是软件工程师、架构师、开发组长、还是技术经理,软件设计原则对你都非常重要。软件工程是一门关于信息决策、创造商业价值、为未来做准备的技术。要记住:

设计原则是你的指南针,是你的北斗星。

它们能为你指明方向,增加你成功的概率,但是最终,还是要你自己决定什么样的方式最适合你的系统。作为一名软件工程师或者架构师,你的工作就是根据金钱时间能力,提出对实现商业目标最好的解决方案。如果你意识到自己角色的重要性,你就应该让自己的思想开放,用各种视角考虑问题。最“干净”的方案不一定总是最好的方案,因为可能会花费更多的开发时间或者会引入一些不必要的管理成本比如,解耦合和过度设计之间的那条线就非常细你的工作就是识别出这些诱惑,避免带来美好的想象向着那些似乎酷毙了的方向越走越远。根据业务需求做出决策,在伸缩性、弹性、高可用、成本、推向市场的时间之间做出权衡。

要实事求是。如果你确信对你的业务有利,对你的软件有利,就不要怕打破这些规则。

每个系统都是不同的每家公司的需求也是不同的,你要发现自己和以前和别的工程师工作场景的不同之处。构建可伸缩软件的路不止一条,技术要好,开发工具要顺手,还要去发现业务驱动因素希望本章提到的各种设计原则为你设计高质量软件系统开了个好头Come on!

 

九 伸缩性的其它维度

本书主要内容是关于设计与开发可伸缩的Web应用的一些技术细节,这些内容的主要受众是软件工程师。事实上,开发一个可伸缩的系统不只是写代码,还有其它可伸缩的维度也需要关注。

  • 运维可伸缩性生产环境能够运行多少台服务器?系统一旦部署在上百台服务器上,如何高效地管理这些服务器就会成为一个挑战。如果每增加20台服务器就需要多招一个运维管理员,那么运维就没法快速廉价地伸缩

  • 个人影响力的伸缩性你个人能为客户和公司创造多大的价值?随着创业公司的成长,你个人的影响力也应该同步增长通过扩展你的职责范围对各个业务主管施加影响,你的工作效率个人绩效会变得更高。

  • 工程师团队的伸缩性创业公司的工程师人数达到多少后工作效率公下降?随着公司的成长,公司需要在不影响工作效率的情况下招募更多的工程师壮大技术部门。这意味着需要建立良好的工程师文化规划合理的团队和系统架构,保证大家能够可伸缩地 并行开发与协作

 

优先级=价值/成本 

 

posted @ 2018-06-20 23:44  沧海一滴  阅读(335)  评论(0编辑  收藏  举报