这是我阅读《从零开始学架构》(作者李运华)所做的读书笔记。
我想以这种慢的方式,去追求理解这本书,学好这本书。
立个FLAG:死磕自己,读完全书,笔记跟进。
架构设计的主要目的就是为了解决软件复杂性带来的问题。这是初心,更是架构设计的准则。
明确了初心,也就找到了软件架构设计的核心。接下来要怎么做呢?无非就是通过熟悉理解需求,找到系统复杂性所在的地方,然后针对这些复杂点进行架构设计。
至于那些传说的高性能、高可用、高扩展的点,只要不是复杂点,并不需要面面俱到,关键是要有针对性的解决问题。同样在参考业界流行的架构设计时,也要关注人家背后解决的复杂问题点,只有复杂点相似的架构设计才具有参考意义,否则为了流行而流行的拿来主义就是在耍流氓。
而软件系统复杂度的来源主要有哪几种呢?主要有高性能、高可用、可扩展、低成本、安全、规模。
1. 高性能
软件系统高性能的复杂度主要体现在两个方面:一是单台计算机内部为了高性能带来的复杂度的问题;而是多台计算机为了高性能带来的复杂度。
单机复杂度
操作系统是软件系统的运行环境,所以操作系统的复杂度也直接决定了软件系统的复杂度。而操作系统和性能最相关的就是进程和线程。
为了完成一个高性能的软件系统,我们需要考虑的如下技术点:多进程、多线程、进程间通信、多线程并发等。这些是针对单机性能优化的核心技术,也正是基于这个原因,在java开发招聘需求中都会对并发编程的能力有所要求。如今我才能真正敢说,为啥非得要求掌握这项技能点了。
集群复杂度
通过大量机器来提升性能,并不仅仅是增加机器这么简单,要让多台机器配合起来达到高性能的目的,是一个复杂的任务。
第一遍读这句话时,一扫也就过去,并没有什么想法,但再次细品这句话时,才意识到自己当下的无知,就在前天我还在建议下游提供服务的系统,简单通过加机器的方式提升性能,因为他们一直是单机提供服务。原本已经是集群的,加机器可能是件相对容易的事,但原本是单机的,要转变为集群,就相对复杂很多。难怪他们一直拖着不给加机器,此刻才算悟出来道理。
下面针对几种常见的方式梳理一下。
任务分配
任务分配的意思是指每天机器都可以处理完整的业务任务,不同的任务分配到不同的机器上执行。
当服务器从1台演变为2台时,需要增加一个任务分配器,这个任务分配器可以是硬件网络设备、软件网络设备、负载均衡软件,还可以是自己开发的系统,而选择合适的任务分配器是一件复杂的事情,需要综合考虑性能、成本、可维护性、可用性等方面的因素。
另外,还需要选择任务分配器与业务服务器之间合适的连接与交互方式。同时还需要为任务分配器增加分配算法,比如:轮询算法,或是按权重分配,诱惑着是按照负载分配,如果是按照负载分配还需要业务服务器上报自己的状态。
当我们继续提供性能要求,继续增多业务服务器数量时,任务分配器本身就可能会成为性能瓶颈。此时任务分配器也需要扩展为多台。此时就又出现了新的复杂点,需要将不同用户分配到不同的任务分配器上,常见的方法包括DNS轮询、CDN、GSLB设备(全局复杂均衡)。
在公司进行异地多活架构设计中,就有用到GSLB,当时我只是简单顺着意思明白其用途,现如今通过反向增加业务服务器数量的推出需要增加任务分配器数量,之后又针对用户分配任务分配器的问题引入GSLB,才真正明白GSLB真正用途。学习了,真是学习了。
另外,多个任务分配器与业务服务器从简单的“一对多”变成了“多对多”的网状结构。数量的增加,关系的复杂,随之状态管理、故障处理的复杂度也随之大大增加。
任务分解
通过任务分配的方式,我们能突破单台机器处理性能的瓶颈,通过增加更多的机器来满足业务的性能需要,但如果业务本身也越来越复杂,单纯只通过任务分配的方式来扩展性能,收益会越来越低。因此,我们引入了第二种方式:任务分解。
为何通过任务分解能够提升性能呢?主要有以下几方面的因素。
(1)简单的系统更容易做到高性能
系统的功能越简单,影响性能的点就越少,就更容易进行有针对性的优化。
(2)可以针对单个任务进行扩展
当各个逻辑任务分解到独立的子系统后,整个系统的性能瓶颈就容易找到,之后针对其进行性能优化或提升,过程中不需要改动整个系统,风险会小很多。
然而系统并不是拆分的越细越好,因为这样为了完成某个业务,系统间调用的次数就会呈指数级上升,而系统间的调用通道目前都是通过网络传输的方式,性能远比系统内的函数调用要低很多。所以架构设计中把握好这个拆分的粒度。
2. 高可用
系统高可用的方案五花八门,但万变不离其宗,本质上都是通过“冗余”来实现高可用。从形式来看,和高性能一样,都是通过增加机器来达到目的,但本质上有根本区别:高性能增加机器的目的在于“扩展”处理性能;而高可用增加机器的目的在于“冗余”处理单元。
通过冗余增加可用性,同样会带来复杂性,下面根据应用场景来梳理一下。
计算高可用
计算高可用同样会产生集群任务分配的复杂度,这个和高性能中提到的是一样的。而常见的双机算法有主备、主主。主备方案又可以细分为冷备、温备和热备。具体应该采用哪种方式,需要结合实际业务需要来分析和判断。
存储高可用
无论正常情况下的传输延迟,还是异常情况下的传输中断,都会导致系统数据在某个时间点或时间段是不一致的,而数据的不一致又会导致业务问题;但如果完全不做冗余,系统整体的高可用又无法保证,所以存储高可用的难点不在于如何备份数据,而在于如何减少或规避数据不一致对业务造成的影响。
著名的CAP理论,从理论上论证了存储高可用的复杂度,也就是说,存储高可用不可能同时满足“一致性、可用性、分区容错性”,最多满足其中两个。
高可用状态决策
通过冗余来实现的高可用系统,在状态决策的本质上就不可能完全做到正确。而常见的决策方式又存在不同程度的问题。
独裁式决策:独裁式因为只有一个决策者,所以不会出现决策混乱,但也因此会出现本身的单点故障。但如果决策者本身又做一套状态决策,就会陷入一个递归的死循环。
协商式决策:两个独立的个体通过交流信息,然后根据规则进行决策,最常见的就是主备决策。协商式的架构和规则都不复杂,其难点在于两者信息交换出现了问题如何应对。在某些场景,这种决策方式总是存在一些问题的。
民主式决策:通过多个个体选举投票的方式进行状态决策。民主式决策存在一个固有的缺陷:脑裂。而脑裂出现的根本原因是原来统一的集群因为连接中断,造成两个独立分割的子集群,每个子集群单独进行选举,于是选出2个主机,相当于人体有两个大脑了。
为了解决脑裂问题,民主式决策的系统,一般都采用“投票节点必须超过系统总节点数一半”规则来处理,这种方式虽然解决了脑裂问题,但同时也降低了系统的整体可用性。
可以看出来,无论采用什么样的方案,状态决策都不可能做到任何场景下都没有问题,如何选取就是一个复杂的分析、判断和选择的过程了。
3. 可扩展
在软件开发领域,面向对象思想的提出,就是为了解决可扩展性带来的问题,后来的设计模式更是将其做到了极致。
并发编程:处理单机的性能问题;
设计模式:处理软件系统的扩展性问题。一下子就看出来了,做优秀的IT从业者为什么要学习并发编程和设计模式,搞不定这个两个点,不可能成为优秀的编程开发人员。
设计具备良好可扩展性的系统,有两个基本条件:正确预测变化、完美封装变化。而这本身就是一件很复杂的事情,其实也正是通过预测变化,来选取合适的设计模式和规则引擎来进行应对将来可能的变化。这里面更多需要使用经验,同时也需要学好设计模式。
4. 低成本
低成本本质上是与高性能、高可用冲突的,所以很多时候低成本不会是架构设计的首要目标,而是架构设计的附加约束。
5. 安全
从技术的角度来讲,安全主要分为两类:一类是功能上的安全,另一类是架构上的安全。
功能安全:就是防小偷,本质上是系统有漏洞,黑客有了可乘之机。常见的有XSS攻击、CSRF攻击、SQL注入、密码破解等。而功能安全是一个逐步完善的过程,往往都是在问题出现后才能有针对性的解决方案。
架构安全:传统的架构安全主要是依靠防火墙,然而防火墙的功能虽然强大,但性能一般,所以在传统的银行和企业应用领域较多,但在互联网领域应用场景并不多。而针对互联网场景的DDOS攻击,一般不会采用防火墙,因为DDOS攻击最大的影响是大量消耗机房的出口总带宽。不管防火墙处理能力有多强,当出口带宽被耗尽时,整个业务在业务看来就是不可用的。
6. 规模
规模带来的复杂度主要原因是量变引起质变,当数量超过一定的阈值后,复杂度会发生质的变化。
功能越来越多,导致系统复杂度指数级上升。数据越来越多,系统复杂度也会发生质变。