凤凰架构
这是什么
笔者认为技术人员成长有一“捷径”,学技术不仅要去看、去读、去想、去用,更要去说、去写。
什么是“凤凰架构”
尽管有“失败是成功之母”这样的谚语,但我们东方人的骨子里更注重的还是一次把事做对做好,尽量别出乱子;而西方人则要“更看得开”一些,把出错看做正常甚至是必须的发展过程,只要出了问题能够兜底使其重回正轨便好。
可靠的系统
如果一项工作要经过多个“不靠谱”的过程相互协作来完成,其中的误差应会不断地累积叠加,导致最终结果必然不能收敛稳定才对。
某个具体的零部件可能会崩溃消亡,但在存续生命的微生态系统中一定会有其后代的出现,重新代替该零部件的作用,以维持系统的整体稳定。在这个微生态里,每一个部件都可以看作一只不死鸟(Phoenix),它会老迈,而之后又能涅槃重生。
架构的演进
架构风格: 大型机(mainframe) ---> 原始分布式(Distributed) ----> 大型单体(Monolithic) ----> 面相服务(Service-Oriented) ---> 微服务 ----> 服务网格 ----> 无服务
架构演变最重要的驱动力,或者说这种“从大到小”趋势的最根本的驱动力,始终都是为了方便某个服务能够顺利地“死去”与“重生”而设计的,个体服务的生死更迭,是关系到整个系统能否可靠续存的关键因素。
流水不腐,有老朽,有消亡,有重生,有更迭才是生态运行的合理规律。
演进中的架构
架构并不是“发明”出来的,是持续进化的结果。
我们将从这些概念起源的最初,去分析它们是什么、它们取代了什么、以及它们为什么能够在斗争中取得成功,为什么变得不可或缺的支撑,又或者它们为什么会失败,在竞争中被淘汰,或逐渐湮灭于历史的烟尘当中。
架构师的视角
作为一个架构师,你应该在做架构设计时思考哪些问题,有哪些主流的解决方案和行业标准做法,各种方案有什么优点、缺点,不同的解决方法会带来什么不同的影响,等等。
分布式的基石
在云原生时代、后微服务时代中,软件与硬件之间的界线已经彻底模糊,无论是基础设施的运维人员,抑或技术平台的开发人员,都有必要深入理解基础设施不变性的目的、原理与实现途径。
技术方法论
笔者认为对于一个技术人员,成长主要的驱动力是实践,在开发程序、解决问题中增长自身的知识,再将知识归纳、总结、升华成为理论的, 是希望大家能先去实践,再谈理论。
运行程序
让后端服务保持无状态,而把状态维持在前端中的设计,对服务的伸缩性和系统的鲁棒性都有着极大的益处,多数情况下都是值得倡导的良好设计。
微服务: spring cloud
制约软件质量与业务能力提升的最大因素是人而非硬件。多数企业即使有钱也很难招到大量的靠谱的开发者。此时,无论是引入外包团队,抑或是让少量技术专家带着大量普通水平的开发者去共同完成一个大型系统就成为了必然的选择。在单体架构下,没有什么有效阻断错误传播的手段,系统中“整体”与“部分”的关系没有物理的划分,系统质量只能靠研发与项目管理措施来尽可能地保障,少量的技术专家很难阻止大量螺丝钉式的程序员或者不熟悉原有技术架构的外包人员在某个不起眼的地方犯错并产生全局性的影响,并不容易做出整体可靠的大型系统。
微服务:Kubernetes
对于团队的开发人员、设计人员、架构人员来说,并没有感觉到工作变得轻松,微服务中的各种新技术名词,如配置中心、服务发现、网关、熔断、负载均衡等等,就够一名新手学习好长一段时间;从产品角度来看,各种 Spring Cloud 的技术套件,如 Config、Eureka、Zuul、Hystrix、Ribbon、Feign 等,也占据了产品的大部分编译后的代码容量。之所以微服务架构里,我们选择在应用层面而不是基础设施层面去解决这些分布式问题,完全是因为由硬件构成的基础设施,跟不上由软件构成的应用服务的灵活性的无奈之举。当 Kubernetes 统一了容器编排管理系统之后,这些纯技术性的底层问题,便开始有了被广 泛认可和采纳的基础设施层面的解决方案。
得益于 Spring Framework 4 中的 Conditional Bean 等声明式特性的出现,对于近年来新发布的 Java 技术组件,声明式编程 (Declarative Programming)已经逐步取代命令式编程 (Imperative Programming)成为主流的选择,使得代码几乎不会与具体技术实现产生耦合,若要更换一种技术实现,只需要调整配置中的声明便可做到。
服务架构演进史
原始分布式时代
“调用远程方法”与“调用本地方法”尽管只是两字之差,但若要同时兼顾简单、透明、性能、正确、鲁棒、一致等特点的话,两者的复杂度就完全不可同日而语了。光是“远程”二字带来的网络环境下的新问题,譬如,远程的服务在哪里(服务发现),有多少个(负载均衡),网络出现分区、超时或者服务出错了怎么办(熔断、隔离、降级),方法的参数与返回结果如何表示(序列化协议),信息如何传输(传输协议),服务权限如何管理(认证、授权),如何保证通信安全(网络安全层),如何令调用不同机器的服务返回相同的结果(分布式数据一致性)等一系列问题。
单体系统时代
正是随着软件架构演进,构筑可靠系统从“追求尽量不出错”,到正视“出错是必然”的观念转变,才是微服务架构得以挑战并逐步开始取代运作了数十年的单体架构的底气所在。
单体系统的真正缺陷不在如何拆分,而在拆分之后的隔离与自治能力上的欠缺。由于所有代码都运行在同一个进程空间之内,所有模块、方法的调用都无须考虑网络分区、对象复制这些麻烦的事和性能损失。获得了进程内调用的简单、高效等好处的同时,也意味着如果任何一部分代码出现了缺陷,过度消耗了进程空间内的资源,所造成的影响也是全局性的、难以隔离的。譬如内存泄漏、线程爆炸、阻塞、死循环等问题,都将会影响整个程序,而不仅仅是影响某一个功能、模块本身的正常运作。如果消耗的是某些更高层次的公共资源,譬如端口号或者数据库连接池泄漏,影响还将会波及整台机器,甚至是集群中其他单体副本的正常工作。
SOA时代
就将这些主数据,连同其他可能被各子系统使用到的公共服务、数据、资源集中到一块,成为一个被所有业务系统共同依赖的核心(Kernel,也称为 Core System),具体的业务系统以插件模块(Plug-in Modules)的形式存在,这样也可提供可扩展的、灵活的、天然隔离的功能特性,即微内核架构。对于平台型应用来说,如果我们希望将新特性或者新功能及时加入系统,微内核架构会是一种不错的方案。
开发信息系统毕竟不是作八股文章,过于精密的流程和理论也需要懂得复杂概念的专业人员才能够驾驭。
SOA 最终没有获得成功的致命伤与当年的EJB 如出一辙,尽管有 Sun Microsystems 和 IBM 等一众巨头在背后力挺,EJB 仍然败于以 Spring、Hibernate 为代表的“草根框架”,可见一旦脱离人民群众,终究会淹没在群众的海洋之中,连信息技术也不曾例外过。
应用受架构复杂度的牵绊却是越来越大,已经距离“透明”二字越来越远了,这是否算不自觉间忘记掉了当年的初心?接下来我们所谈论的微服务时代,似乎正是带着这样的自省式的问句而开启的。
微服务时代
“微服务是一种通过多个小型服务组合来构建单个应用的架构风格,这些服务围绕业务能力而非特定的技术标准来构建。各个服务可以采用不同的编程语言,不同的数据存储技术,运行在不同的进程之中。服务采取轻量级的通信机制和自动化的部署机制实现通信与运维。
容错性设计: 不再虚幻地追求服务永远稳定,而是接受服务总会出错的现实,要求在微服务的设计中,有自动的机制对其依赖的服务能够进行快速故障检测,在持续出错的时候进行隔离,在服务恢复的时候重新联通。所以“断路器”这类设施,对实际生产环境的微服务来说并不是可选的外围组件,而是一个必须的支撑点,如果没有容错性的设计,系统很容易就会被因为一两个服务的崩溃所带来的雪崩效应淹没。可靠系统完全可能由会出错的服务组成,这是微服务最大的价值所在
演进式设计:容错性设计承认服务会出错,演进式设计则是承认服务会被报废淘汰。一个设计良好的服务,应该是能够报废的,而不是期望得到长存永生。
服务的注册发现、跟踪治理、负载均衡、故障隔离、认证授权、伸缩扩展、传输通信、事务处理,等等,这些问题,在微服务中不再会有统一的解决方案,即使只讨论 Java 范围内会使用到的微服务, 光一个服务间远程调用问题,可以列入解决方案的候选清单的就有:RMI(Sun/Oracle)、Thrift(Facebook)、Dubbo(阿里巴巴)、gRPC(Google)、Motan2(新浪)、Finagle(Twitter)、brpc(百度)、Arvo(Hadoop)、JSON-RPC、REST,等等; 光一个服务发现问题,可以选择的就有:Eureka(Netflix)、Consul(HashiCorp)、Nacos(阿里巴巴)、ZooKeeper(Apache)、Etcd(CoreOS)、CoreDNS(CNCF),等等。其他领域的情况也是与此类似,总之,完全是八仙过海,各显神通的局面。
后微服务时代
从软件层面独力应对分布式架构所带来的各种问题,发展到应用代码与基础设施软、硬一体,合力应对架构问题的时代,现在常被媒体冠以“云原生”这个颇为抽象的名字加以宣传。
无服务时代
虽然在顺序上笔者将“无服务”安排到了“微服务”和“云原生”时代之后,但它们两者并没有继承替代关系。
我们谈历史,重点不在考古,而是借历史之名,理解好每种架构出现的意义与淘汰的原因,为的是更好地解决今天的现实问题,寻找出未来架构演进的发展道路。
远程服务调用
RPC的三个问题: 1. 如何表示数据 2. 如何传递数据 3. 如何确定方法
现在已经相继出现过 RMI(Sun/Oracle)、Thrift(Facebook/Apache)、Dubbo(阿里巴巴/Apache)、gRPC(Google)、Motan1/2(新浪)、Finagle(Twitter)、brpc(百度/Apache)、.NET Remoting(微软)、Arvo(Hadoop)、JSON-RPC 2.0(公开规范,JSON-RPC 工作组)……等等难以穷举的协议和框架。
今时今日,任何一款具有生命力的 RPC 框架,都不再去追求大而全的“完美”,而是有自己的针对性特点作为主要的发展方向
gRPC 和 Thrift 都有自己优秀的专有序列化器,而传输协议方面,gRPC 是基于 HTTP/2 的,支持多路复用和 Header 压缩,Thrift 则直接基于传输层的 TCP 协议来实现,省去了额外应用层协议的开销。
经历了 RPC 框架的战国时代,开发者们终于认可了不同的 RPC 框架所提供的特性或多或少是有矛盾的,很难有某一种框架说“我全部都要”。
功能多起来,协议就要弄得复杂,效率一般就会受影响;要简单易用,那很多事情就必须遵循约定而不是配置才行;要重视效率,那就需要采用二进制的序列化器和较底层的传输协议,支持的语言范围容易受限。也正是每一种 RPC 框架都有不完美的地方,所以才导致不断有新的 RPC 轮子出现,决定了选择框架时在获得一些利益的同时,要付出另外一些代价。
REST设计风格
REST,即“表征状态转移,REST只能说是风格而不是规范、协议,并且能完全达到 REST 所有指导原则的系统也是不多见的
我们不应该忽略 Fielding 的身份和此前的工作背景,这些信息对理解 REST 的设计思想至关重要。
REST 的另一条核心原则,REST 希望开发者面向资源编程,希望软件系统设计的重点放在抽象系统该有哪些资源上,而不是抽象系统该有哪些行为(服务)上。
REST 的基本思想是面向资源来抽象问题,它与此前流行的编程思想——面向过程的编程在抽象主体上有本质的差别。
面向资源的编程思想与另外两种主流编程思想只是抽象问题时所处的立场不同,只有选择问题,没有高下之分。
本地事务
本地事务是最基础的一种事务解决方案,只适用于单个服务使用单个数据源的场景。从应用角度看,它是直接依赖于数据源本身提供的事务能力来工作的。
持久性与原子性
为了能够顺利地完成崩溃恢复,在磁盘中写入数据就不能像程序修改内存中变量值那样,直接改变某表某行某列的某个值,而是必须将修改数据这个操作所需的全部信息,包括修改什么数据、数据物理上位于哪个内存页和磁盘块中、从什么值改成什么值,等等,以日志的形式——即仅进行顺序追加的文件写入的形式(这是最高效的写入方式)先记录到磁盘中。只有在日志记录全部都安全落盘,数据库在日志中看到代表事务成功提交的“提交记录”(Commit Record)后,才会根据日志上的信息对真正的数据进行修改,修改完成后,再在日志中加入一条“结束记录”(End Record)表示事务已完成持久化,这种事务实现方法被称为“Commit Logging”(提交日志)。
commit logging保证了持久性和原子性,首先,日志一旦成功写入 Commit Record,那整个事务就是成功的,即使真正修改数据时崩溃了,重启后根据已经写入磁盘的日志信息恢复现场、继续修改数据即可,这保证了持久性;其次,如果日志没有成功写入 Commit Record 就发生崩溃,那整个事务就是失败的,系统重启后会看到一部分没有 Commit Record 的日志,那将这部分日志标记为回滚状态即可,整个事务就像完全没好有发生过一样,这保证了原子性。
Write-Ahead Logging 允许 NO-FORCE,也允许 STEAL,它给出的解决办法是增加了另一种被称为 Undo Log 的日志类型,当变动数据写入磁盘前,必须先记录 Undo Log,注明修改了哪个位置的数据、从什么值改成什么值,等等。以便在事务回滚或者崩溃恢复时根据Undo Log 对提前写入的数据变动进行擦除。Undo Log 现在一般被翻译为“回滚日志”,此前记录的用于崩溃恢复时重演数据变动的日志就相应被命名为 Redo Log,一般翻译为“重做日志”。
undo lo、redo log、 commit logging
三种锁:写锁、读锁、范围锁
要在并发下实现串行的数据访问该怎样做?几乎所有程序员都会回答:加锁同步呀!正确,现代数据库均提供了以下三种锁: 1.写锁: write lock也叫排它锁exclusive lock,简写为X-lock, 其他事务不能加读锁 2.读锁: read lock也叫排它锁shared lock,,简写S-lock,其他事物可以加读锁 3.范围锁
写锁(Write Lock,也叫作排他锁,eXclusive Lock,简写为 X-Lock):如果数据有加写锁,就只有持有写锁的事务才能对数据进行写入操作,数据加持着写锁时,其他事务不能写入数据,也不能施加锁。
读锁(Read Lock,也叫作共享锁,Shared Lock,简写为 S-Lock):多个事务可以对同一个数据添加多个读锁,数据被加上读锁后就不能再被加上写锁,所以其他事务不能对该数据进行写入,但仍然 可以读取。对于持有读锁的事务,如果该数据只有它自己一个事务加了读锁,允许直接将其升级为写锁,然后写入数据。
范围锁(Range Lock):对于某个范围直接加排他锁,在这个范围内的数据不能被写入。
MVCC,多版本并发控制的基本思路是对数据库的任何修改都不会直接覆盖之前的数据,而是产生一个新版副本与老版本共存,以此达到读取时可以完全不加锁的目的。在这句话中,“版本”是个关键词
MVCC 是只针对“读+写”场景的优化,如果是两个事务同时修改数据,即“写+写”的情况,那就没有多少优化的空间了,此时加锁几乎是唯一可行的解决方案,稍微有点讨论余地的是加锁的策略是“乐观加锁(Optimistic Locking)还是“悲观加锁”(Pessimistic Locking)。
全局事务
本节所讨论的内容是一种在分布式环境中仍追求强一致性的事务处理方案,对于多节点而且互相调用彼此服务的场合(典型的就是现在的微服务系统)是极不合适的,今天它几乎只实际应用于单服务多数据源的场合中。
JTA基于XA模式在java的实现: java transaction api,主要两个接口: 1.javax.transaction.TransactionManager 2.javax.transaction.xa.XAResource
XA将事务分为两个阶段(两段式提交也就是2pc 2 phase commit): 1.准备阶段,所有事务准备提交,都准备好返回Prepared 2.提交阶段
三段式提交,把准备阶段分为:Cancommit,Precommit 1.cancommit 2.precommit 3.docommit
两段式、三段式有两个前提假设: 1.必须假设网络在提交阶段的短时间是可靠的,即提交阶段不会丢失消息 1.必须假设因为网络分区、机器崩溃或者其他原因而导致的的节点最终能恢复。
共享事务
没有反过来代理一个数据库为多个应用提供事务协调的交易服务代理。这也是说它更有可能是个伪需求的原因,如果你有充足理由让多个微服务去共享数据库,就必须找到更加站得住脚的理由来向团队解释拆分微服务的目的是什么才行。
分布式事务
CAP:consistency、availability、network partitions, you can have at most two of these properties for any shared-data-system
一致性、可用性、分区容错性
可用性与这两个指标相关:可靠性、可维护性,可靠性使用平均无故障时间(Mean time between failure, MTBF)来度量;可维护性使用平均修复时间(mean time to repair, MTTR)来度量。 A = MTBF/(MTBF+MTTR)
选择放弃一致性的 AP 系统目前是设计分布式系统的主流选择
强一致性、最终一致性、弱一致性
可靠事件队列
最终一致性总结了了一种独立于 ACID 获得的强一致性之外的、使用 BASE 来达成一致性目的的途径。BASE 分别是基本可用性(Basically Available)、柔性事务(Soft State)和最终一致性(Eventually Consistent)的缩写。
由此可见,可靠事件队列只要第一步业务完成了,后续就没有失败回滚的概念,只许成功,不许失败。这种靠着持续重试来保证可靠性的解决方案,也有了专门的名字叫作“最大努力交付"。譬如 TCP 协议中未收到 ACK 应答自动重新发包的可靠性保障就属于最大努力交付。
可靠性事件队列,会存在超卖的现象。
账户扣款---> 仓库出货 --> 商家收款。 只要第一步扣款成功,后续就一直发幂等的消息,如果仓库超卖,则人工介入补货。
TCC事务
TCC 是另一种常见的分布式事务机制,它是“Try-Confirm-Cancel”三个单词的缩写。
可靠消息队列虽然能保证最终的结果是相对可靠的,过程也足够简单(相对于TCC 来说),但整个过程完全没有任何隔离性可言,如果业务需要隔离,那架构师通常就应该重点考虑 TCC 方案,该方案天生适合用于需要强隔离性的分布式事务中。
TCC 并非纯粹只有好处,它也带来了更高的开发成本和业务侵入性,意味着有更高的开发成本和更换事务实现方案的替换成本,所以,通常我们并不会完全靠裸编码来实现 TCC,而是基于某些分布式事务中间件(譬如阿里开源的Seata )去完成,尽量减轻一些编码工作量
SAGA事务
TCC 事务具有较强的隔离性,避免了“超售”的问题,而且其性能一般来说是本篇提及的几种柔性事务模式中最高的,但它仍不能满足所有的场景。如果用户、商家的账号余额由银行管理的话,其操作权限和数据结构就不可能再随心所欲的地自行定义,通常也就无法完成冻结款项、解冻、扣减这样的操作,因为银行一般不会配合你的操作。所以 TCC 中的第一步 Try 阶段往往无法施行。
SAGA 由两部分操作组成: 1. 大事务拆分为若干个小事务,将整个分布式事务T分解为n个子事务,命名为T1,T2....Ti,每个子事务都被视为原子行为 2. 为每个事务设计对应的补偿动作,命名为C1,C2.....Ci Ti与Ci必须满足一下条件: 1.Ti与Ci都具备幂等性 2.Ti与Ci满足交换律,即先执行Ti还是先执行Ci,其效果都是一样的
两种恢复策略: 1.正向恢复,如果Ti事务提交失败,则一直对Ti进行重试,直至成功 2.反向恢复,如果Ti事务提交失败,则一直执行Ci对Ti进行补偿,直至成功
与 TCC 相比,SAGA 不需要为资源设计冻结状态和撤销冻结的操作,补偿操作往往要比冻结操作容易实现得多。 前面提到的账号余额直接在银行维护的场景,扣款成功后,但是后续仓库出货失败,可以退款作为补偿措施是可行的。
分布式事务中没有一揽子包治百病的解决办法,因地制宜地选用合适的事务处理方案才是唯一有效的做法。
透明多级分流系统
“分布广阔”源于“多级”,“意识不到”谓之“透明”。
在能满足需求的前提下,最简单的系统就是最好的系统。
客户端缓存
在 HTTP 协议设计之初,便确定了服务端与客户端之间“无状态”(Stateless)的交互原则。但无状态并不只有好的一面,由于每次请求都是独立的,服务端不保存此前请求的状态和资源,所以也不可避免地导致其携带有重复的数据,造成网络性能降低。HTTP 协议对此问题的解决方案便是客户端缓存,在 HTTP 从 1.0 到 1.1,再到 2.0 版本的每次演进中,逐步形成了现在被称为“状态缓存”、“强制缓存”(许多资料中简称为“强缓存”)和“协商缓存”的 HTTP 缓存机制。
Etag 是HTTP服务器的响应 Header,用于告诉客户端这个资源的唯一标识。HTTP 服务器可以根据自己的意愿来选择如何生成这个标识
域名解析
- 客户端先检查本地的 DNS 缓存,查看是否存在并且是存活着的该域名的地址记录。
- 客户端将地址发送给本机操作系统中配置的本地 DNS(Local DNS),这个本地 DNS 服务器可以由用户手工设置,也可以在 DHCP 分配时或者在拨号时从 PPP 服务器中自动获取到。
- 本地 DNS 收到查询请求后,会按照“是否有 www.icyfenix.com.cn 的权威服务器”→“是否有 icyfenix.com.cn 的权威服务器”→“是否有 com.cn 的权威服务器”→“是否有 cn 的权威服务器”的顺序,依 次查询自己的地址记录,如果都没有查询到,就会一直找到最后点号代表的根域名服务器为止。
- 现在假设本地 DNS 是全新的,上面不存在任何域名的权威服务器记录,所以当 DNS 查询请求按步骤 3 的顺序一直查到根域名服务器之后,它将会得到“ cn 的权威服务器”的地址记录,然后通过“ cn 的权 威服务器”,得到“ com.cn 的权威服务器”的地址记录,以此类推,最后找到能够解释 www.icyfenix.com.cn 的权威服务器地址。
传输链路
由此可见,一旦在技术根基上出现问题,依赖使用者通过各种 Tricks 去解决,无论如何都难以摆脱“两害相权取其轻”的权衡困境,否则这就不是 Tricks 而是会成为一种标准的设计模式了。
连接数优化
持久连接的原理是让客户端对同一个域名长期持有一个或多个不会用完即断的 TCP 连接。典型做法是在客户端维护一个 FIFO 队列,每次取完数据(如何在不断开连接下判断取完数据将会放到稍后传输压缩部分去讨论)之后一段时间内不自动断开连接,以便获取下一个资源时直接复用,避免创建 TCP 连接的成本。但问题是处理结果无法及时返回客户端,服务端不能哪个请求先完成就返回哪个,更不可能将所有要返回的资源混杂到一起交叉传输,原因是只使用一个 TCP 连接来传输多个资源的话,如果顺序乱了,客户端就很难区分哪个数据包归属哪个资源了。
队首阻塞问题一直持续到第二代的 HTTP 协议,即 HTTP/2 发布后才算是被比较完美地解决。譬如请求的 Headers、Body,或者用来做控制标识,譬如打开流、关闭流。这里说的流(Stream)是一个逻辑上传输链路的数据通道概念,每个帧都附带一个流 ID 以标识这个帧属于哪个流。这样,在同一个 TCP连接中传输的多个数据帧就可以根据流 ID 轻易区分出开来,在客户端毫不费力地将不同流中的数据重组出不同 HTTP 请求和响应报文来。这项设计是 HTTP/2 的最重要的技术特征一,被称为 HTTP/2 多路复用 (HTTP/2 Multiplexing)技术
目前网络链路传输领域正处于新旧交替的时代,许多既有的设备、程序、知识都会在未来几年时间里出现重大更新。
内容分发网络
如果把某个互联网系统比喻为一家企业,那内容分发网络就是它遍布世界各地的分支销售机构。
内容分发网络的工作过程,主要涉及路由解析、内容分发、负载均衡和所能支持的 CDN 应用内容四个方面。
CDN 获取源站资源的过程被称为“内容分发”,“内容分发网络”的名字正是由此而来,可见这是 CDN 的核心价值。目前主要有以下两种主流的内容分发方式: 1.主动分发push 2.被动回源pull
负载均衡
负载均衡算法:
1.轮询均衡
2.权重轮询均衡
3.随机均衡
4.权重随机均衡
5.一致性hash均衡
6.响应速度均衡
7.最少连接数均衡
均衡负载器分为:软件均衡器、硬件均衡器 硬件均衡器: F5、 A10 软件均衡器: 操作系统内核的:lvs(linux virtual server) 应用程序形式的: nginx Haproxy keepalived
服务端缓存
从开发角度来说,引入缓存会提高系统复杂度,因为你要考虑缓存的失效、更新、一致性等问题,缓存虽然是典型以空间换时间来提升性能的手段,但它的出发点是缓解 CPU 和 I/O 资源在峰值流量下的压力,“顺带”而非“专门”地提升响应性能。这里的言外之意是如果可以通过增强 CPU、I/O 本身的性能(譬如扩展服务器的数量)来满足需要的话,那升级硬件往往是更好的解决方案,即使需要一些额外的投入成本,也通常要优于引入缓存后可能带来的风险。
设计或者选择缓存至少会考虑以下四个维度的属性:吞吐量、命中率、扩展功能、分布式支持
缓存淘汰策略: FIFO: first in first out 优先淘汰最早进入缓存的数据 LRU: least recent used 优先淘汰最久未被使用访问过的数据 LFU: least frequently used :优先淘汰最不经常使用的数据
进程内缓存方案:concurrrentHashMap、Ehcache、Guava Cache、Caffeine
分布式缓存
对分布式缓存来说,处理与网络有相关的操作是对吞吐量影响更大的因素,往往也是比淘汰策略、扩展功能更重的关注点
1.频繁更新但甚少读取的数据,通常是不会有人把它拿去做缓存的,因为这样做没有收益。 2.对于甚少更新但频繁读取的数据,理论上更适合做复制式缓存; 3.对于更新和读取都较为频繁的数据,理论上就更适合做集中式缓存
如今Redis 广为流行,基本上已经打败了 Memcached 及其他集中式缓存框架,成为集中式缓存的首选,甚至可以说成为了分布式缓存的实质上的首选,几乎到了不必管读取、写入哪种操作更频繁,都可以无脑上 Redis 的程度。
介绍使用几种常见的缓存风险及其应对办法: 1.缓存穿透 2.缓存击穿 3.缓存雪崩 4.缓存污染
架构安全性
与安全相关的问题,一般不会直接创造价值,解决起来又烦琐复杂,费时费力,因此经常性被开发者有意无意地忽略掉。庆幸的是这些问题基本上也都是与具体系统、具体业务无关的通用性问题,这意味着它们往往会存在着业界通行的、已被验证过是行之有效的解决方案,乃至已经形成行业标准,不需要开发者自己从头去构思如何解决。
一个架构安全性的经验原则:以标准规范为指导、以标准接口去实现。安全涉及的问题很麻烦,但解决方案已相当成熟,对于 99%的系统来说,在安全上不去做轮子,不去想发明创造,严格遵循标准就是最恰当的安全设计。
HTTP认证
注意 Base64 只是一种编码方式,并非任何形式的加密,所以 Basic 认证的风险是显而易见的。
WebAuthn 采用非对称加密的公钥、私钥替代传统的密码,这是非常理想的认证方案,私钥是保密的,只有验证器需要知道它,连用户本人都不需要知道,也就没有人为泄漏的可能;公钥是公开的,可以被任何人看到或存储。公钥可用于验证私钥生成的签名,但不能用来签名,除了得知私钥外,没有其他途径能够生成可被公钥验证为有效的签名,这样服务器就可以通过公钥是否能够解密来判断最终用户的身份是否合法。
授权
授权行为在程序中的应用非常广泛,给某个类或某个方法设置范围控制符(public、protected、private、)在本质上也是一种授权(访问控制)行为。而在安全领域中所说的授权就更具体一些,通常涉及以下两个相对独立的问题: 1.确保授权的过程可靠 2.确保授权的结果可控
RBAC
RBAC: role-base access controller
所有的访问控制模型,实质上都是在解决同一个问题:“谁(User)拥有什么权限(Authority)去操作(Operation)哪些资源(Resource)”。
如果某个系统涉及到成百上千的资源,又有成千上万的用户,一旦两者搅合到一起,要为每个用户访问每个资源都分配合适的权限,必定导致巨大的操作量和极高的出错概率,这也正是 RBAC 所关注的问 题之一。
为了避免对每一个用户设定权限,RBAC 将权限从用户身上剥离,改为绑定到“角色”(Role)上,将权限控制变为对“角色拥有操作哪些资源的许可”
OAuth2
OAuth2 是在RFC 6749 中定义的国际标准,在 RFC 6749 正文的第一句就阐明了 OAuth2 是面向于解决第三方应用(Third-Party Application)的认证授权协议
凭证
cookie-session
一般来说,系统会把状态信息保存在服务端,在 Cookie 里只传输的是一个无字面意义的、不重复的字符串,习惯上以 sessionid 或者 jsessionid 为名,服务器拿这个字符串为 Key,在内存中开辟一块空间,以 Key/Entity 的结构存储每一个在线用户的上下文状态,再辅以一些超时自动清理之类的管理措施。
HMAC哈希算法和普通哈希算法
保密
保密是有成本的,追求越高的安全等级,就要付出越多的工作量与算力消耗。
为了抵御上述风险,保密强度还要进一步提升,譬如银行会使用独立于客户端的存储证书的物理设备(俗称的 U 盾)来避免根证书被客户端中的恶意程序窃取伪造
“普通安全强度”是指在具有一定保密安全性的同时,避免消耗过多的运算资源,验证起来也相对便捷。对多数信息系统来说,只要配合一定的密码规则约束,譬如密码要求长度、特殊字符等,再配合 HTTPS 传输,已足防御大多数风险了。
传输
摘要不可逆,但是可以碰撞,加密与摘要的本质区别在于加密是可逆的,逆过程就是解密
根据加密与解密是否采用同一个密钥,现代密码学算法可分为对称加密算法和非对称加密两大类型
20 世纪 70 年代中后期出现的非对称加密算法从根本上解决了密钥分发的难题,它将密钥分成公钥和私钥,公钥可以完全公开,无须安全传输的保证。
因非对称加密本身的效率所限,难以支持分组,所以主流的非对称加密算法都只能加密不超过密钥长度的数据,这决定了非对称加密不能直接用于大量数据的加密。
在加密方面,现在一般会结合对称与非对称加密的优点,以混合加密来保护信道安全,具体做法是用非对称加密来安全地传递少量数据给通信的另一方,然后再以这些数据为密钥,采用对称加密来安全高效地大量加密传输数据,这种由多种加密算法组合的应用形式被称为“密码学套件”。非对称加密在这个场景中发挥的作用称为“密钥协商”。
三种密码学算法: 1. 哈希摘要 : 主要用作摘要,无法解密;MD2/4/5/6、SHA0/1/256/512 2. 对称加密: 主要用作加密;DES、AES、RC4、IDEA 3. 非对称加密: 主要用作签名传递密钥; RSA、BCDSA、ElGamal
传输安全层
在计算机科学里,隔离复杂性的最有效手段(没有之一)就是分层,如果一层不够就再加一层,这点在网络中更是体现得淋漓尽致。
分布式共识算法
一年中出现系统宕机的概率也许还要高于 5%,这决定了软件系统也必须有多台机器能够拥有一致的数据副本,才有可能对外提供可靠的服务。
分布式系统里面,我们必须考虑动态的数据如何在不可靠的网络通信条件下,依然能在各个节点之间正确复制的问题。
考虑到分布式环境下网络分区现象是不可能消除的,甚至允许不再追求系统内所有节点在任何情况下的数据状态都一致,而是采用“少数服从多数”的原则,一旦系统中过半数的节点中完成了状态的转换,就认为数据的变化已经被正确地存储在系统当中,这样就可以容忍少数(通常是不超过半数)的节点失联,使得增加机器数量对系统整体的可用性变成是有益的,这种思想在分布式中被称为“Quorum 机制 。
Paxos
paxos算法将节点分为三类: 1.提案节点, Raft 算法中就直接把“提案”叫作“日志”了 2.决策节点, 提案一旦得到过半数决策节点的接受,即称该提案被批准(Accept),提案被批准即意味着该值不能再被更改,也不会丢失,且最终所有节点都会接受该它 3.记录节点, 不参与提案,也不参与决策,只是单纯地从提案、决策节点中学习已经达成共识的提案
使用 Paxos 算法的分布式系统里的,所有的节点都是平等的,它们都可以承担以上某一种或者多种的角色,不过为了便于确保有明确的多数派,决策节点的数量应该被设定为奇数个。
在算法实现中会引入随机超时时间来避免活锁的产生。
Basic Paxos 是一种很学术化但对工业化并不友好的算法,现在几乎只用来做理论研究。实际的应用都是基于 Multi Paxos 和 Fast Paxos 算法的,接下来我们将会了解 Multi Paxos 与一些它的理论等价的算法(如 Raft、ZAB 等算法)
Gossip
“强一致性”的分布式共识协议: 尽管系统内部节点可以存在不一致的状态,但从系统外部看来,不一致的情况并不会被观察到,所以整体上看系统是强一致性的
能够容忍网络上节点的随意地增加或者减少,随意地宕机或者重启,新增加或者重启的节点的状态最终会与其他节点同步达成一致。Gossip 把网络上所有节点都视为平等而普通的一员,没有任何中心化节点或者主节点的概念,这些特点使得 Gossip 具有极强的鲁棒性,而且非常适合在公众互联网中应用。
“最终一致性”的分布式共识协议: 这表明系统中不一致的状态有可能会在一定时间内被外部直接观察到。一种典型且极为常见的最终一致的分布式系统就是DNS 系统,在各节点缓存的 TTL 到期之前,都有可能 与真实的域名翻译结果存在不一致。
Gossip两个步骤的简单循环: 1.如果有某一项信息需要在整个网络中所有节点中传播,那从信息源开始,选择一个固定的传播周期(譬如 1 秒),随机选择它相连接的 k 个节点(称为 Fan-Out)来传播消息。 2.每一个节点收到消息后,如果这个消息是它之前没有收到过的,将在下一个周期内,选择除了发送消息给它的那个节点外的其他相邻 k 个节点发送相同的消息,直到最终网络中所有节点都收到了消息, 尽管这个过程需要一定时间,但是理论上最终网络的所有节点都会拥有相同的消息
从类库到服务
服务发现
远程服务调用都是使用全限定名(Fully Qualified Domain Name,FQDN )、端口号与服务标识所构成的三元组来确定一个远程服务的精确坐标。
注册中心不依赖其他服务,但被所有其他服务共同依赖,是系统中最基础的服务,几乎没有可能在业务层面进行容错。这意味着服务注册中心一旦崩溃,整个系统都不再可用。必须尽最大努力保证服务发现的可用 性。实际用于生产的分布式系统,服务注册中心都是以集群的方式进行部署的,通常使用三个或者五个节点(通常最多七个,一般也不会更多了,否则日志复制的开销太高)来保证高可用。
TTL: time to live
可用性与一致性的矛盾,是分布式系统永恒的话题,在服务发现这个场景里,权衡的主要关注点是相对更能容忍出现服务列表不可用的后果,还是出现服务数据不准确的后果,其次才到性能高低,功能是否强大,使用是否方便等因素.
直接以服务发现、服务注册中心为目标的组件库,或者间接用来实现这个目标的工具主要有以下三类: 1.在分布式 K/V 存储框架上自己开发的服务发现,这类的代表是 ZooKeeper、Doozerd、Etcd 2.以基础设施(主要是指 DNS 服务器)来实现服务发现,这类的代表是 SkyDNS、CoreDNS 3.专门用于服务发现的框架和工具,这类的代表是 Eureka、Consul 和 Nacos。
用的人多什么问题都会有解决方案
网关路由
微服务中网关的首要职责就是作为统一的出口对外提供服务,将外部访问网关地址的流量,根据适当的规则路由到内部集群中正确的服务节点之上,因此,微服务中的网关,也常被称为“服务网关”或者“API 网关。
网关 = 路由器(基础职能) + 过滤器(可选职能)
网络IO模型
显而易见,异步 I/O 模型是最方便的,毕竟能叫外卖谁愿意跑饭堂啊,但前提是你学校里有开展外卖业务。同样,异步 I/O 受限于操作系统,Windows NT 内核早在 3.5 以后,就通过IOCP 实现了真正的异步 I/O 模型。而 Linux 系统下,是在 Linux Kernel 2.6 才首次引入,目前也还并不算很完善,因此在 Linux 下实现高并发网络编程时仍是以多路复用 I/O模型模式为主。
流量治理
快速失败(Failfast):一些业务场景是不允许做故障转移的,故障转移策略能够实施的前提是要求服务具备幂等性,对于非幂等的服务,重复调用就可能产生脏数据,引起的麻烦远大于单纯的某次服务调用失败,此时就应该以快速失败作为首选的容错策略。例如调用银行的接口
安全失败(Failsafe): 一个调用链路中的服务通常也有主路和旁路之分,并不见得其中每个服务都是不可或缺的,有部分服务失败了也不影响核心业务的正确性。典型的有审计、日志、调试信息,等等,自动记录一条服务调用出错的日志备查即可
沉默失败(Failsilent): 如果大量的请求需要等到超时(或者长时间处理后)才宣告失败,很容易由于某个远程服务的请求堆积而消耗大量的线程、内存、网络等资源,进而影响到整个系统的稳定。面对这种情况,一种合理的失败策略是当请求失败后,就默认服务提供者一定时间内无法再对外提供服务,不再向它分配请求流量,将错误隔离开来,避免对系统其他部分产生影响,此即为沉默失败策略。
故障恢复(Failback):故障恢复一般不单独存在,而是作为其他容错策略的补充措施,通常默认会采用快速失败加上故障恢复的策略组合。它是指当服务调用出错了以后,将该次调用失败的信息存入一个消息队列中,然后由系统自动开始异步重试调用。
并行调用(Forking): 调用之前就开始考虑如何获得最大的成功概率,并行调用策略很符合人们日常对一些重要环节进行的“双重保险”或者“多重保险”的处理思路,它是指一开始就同时向多个服务副本发起调用,只要有其中任何一个返回成功,那调用便宣告成功
广播调用(Broadcast):广播调用与并行调用是相对应的,都是同时发起多个调用,但并行调用是任何一个调用结果返回成功便宣告成功,广播调用则是要求所有的请求全部都成功,这次调用才算是成功,任何一个服务提供者出现异常都算调用失败,广播调用通常会被用于实现“刷新分布式缓存”这类的操作。
断路器模式是微服务架构中最基础的容错设计模式, 是通过代理(断路器对象)来一对一地(一个远程服务对应一个断路器对象)接管服务调用者的远程请求。断路器会持续监控并统计服务返回的成功、失败、超时、拒绝等各种结果,当出现故障(失败、超时、拒绝)的次数达到断路器的阈值时,它状态就自动变为“OPEN”,后续此断路器代理的远程访问都将直接返回调用失败,而不会发出真正的远程服务请求 通过断路器对远程服务的熔断,避免因持续的失败或拒绝而消耗资源,因持续的超时而堆积请求,最终的目的就是避免雪崩效应的出现。
服务熔断和服务降级之间的联系与差别。断路器做的事情是自动进行服务熔断,这是一种快速失败的容错策略的实现方法。在快速失败策略明确反馈了故障信息给上游服务以后,上游服务必须能够主动处理调用失败的后果,而不是坐视故障扩散,这里的“处理”指的就是一种典型的服务降级逻辑,降级逻辑可以包括,但不应该仅仅限于是把异常信息抛到用户界面去,而应该尽力想办法通过其他路径解决问题,譬如把原本要处理的业务记录下来,留待以后重新处理是最低限度的通用降级逻辑
调用外部服务的故障大致可以分为: 失败: 400 Bad Request、500 Internal Server Error 拒绝: 401 Unauthorized、403 Forbidden 超时: 408 Request Timeout、504 Gateway Timeout 等
线程是典型的整个系统的全局性资源,尤其是 Java 这类将线程映射为操作系统内核线程来实现的语言环境中,为了不让某一个远程服务的局部失败演变成全局性的影响,就必须设置某种止损方案,这便是服务隔离的意义
一般 Java 应用的Tomcat线程池最大只会设置到 200 至 400 之间
重试模式
故障转移和故障恢复策略都需要对服务进行重复调用 当发出的请求收到了 401 Unauthorized 响应,说明服务本身是可用的,只是你没有权限调用。可以根据包括 HTTP 响应码在内的各种具体条件来设置不同的重试参数; 仅对具备幂等性的服务进行重试; 重试必须有明确的终止条件,常用的终止条件有两种: 超时终止、次数终止。通常最多就只重试 2 至 5次
由于重试模式可以在网络链路的多个环节中去实现,譬如客户端发起调用时自动重试,网关中自动重试、负载均衡器中自动重试
熔断、隔离、重试、降级、超时等概念都是建立具有韧性的微服务系统必须的保障措施。目前,这些措施的正确运作,还主要是依靠开发人员对服务逻辑的了解,以及运维人员的经验去静态调整配置参数和阈值
流量控制
当系统资源不足以支撑外部超过预期的突发流量时,便应该要有取舍,建立面对超额流量自我保护的机制,这个机制就是微服务中常说的“限流”。
缓存
如今Redis 广为流行,基本上已经打败了 Memcached 及其他集中式缓存框架,成为集中式缓存的首选,甚至可以说成为了分布式缓存的实质上的首选,几乎到了不必管读取、写入哪种操作更频繁,都可以无脑上 Redis 的程度。
尽管 Redis 最初设计的本意是 NoSQL 数据库而不是专门用来做缓存的,可今天它确实已经成为许多分布式系统中无可或缺的基础设施,广泛用作缓存的实现方案。
容错策略和容错设计模式,最终目的均是为了避免服务集群中某个节点的故障导致整个系统发生雪崩效应,但仅仅做到容错,只让故障不扩散是远远不够的,我们还希望系统或者至少系统的核心功能能够表现出最佳的响应的能力,不受或少受硬件资源、网络带宽和系统中一两个缓慢服务的拖累。
微服务
“对于小型系统,单体架构就是最好的架构”。系统进行任何改造的根本动力都是“这样做收益大于成本”,
足可见技术圈里即使再有本事,也还是需要好好包装一下的道理。
随着时间的流逝,团队对该项目质量的持续保持能力会逐渐下降,一方面是高级技术专家不可能持续参与软件稳定之后的迭代过程,反过来,如果持续绑定在同一个达到稳定之后的项目上,也很难培养出技术专家。
架构腐化是软件动态发展中出现的问题,任何静态的治理方案都只能延缓,不能根治,必须在发展中才能寻找到彻底解决的办法。治理架构腐化唯一有效的办法是演进式的设计,这点与生物族群的延续也很像,户枢不蠹,也只有流水,才能不腐
大型软件的建设是一个不断推倒从来的演进过程,前一个版本对后一个版本的价值在于它满足了这个阶段用户的需要,让团队成功适应了这个阶段的复杂度,可以向下一个台阶迈进。对于最终用户来说,一个能在演进过程中逐步为用户提供价值的系统,体验也要远好于一个憋大招的系统——哪怕这大招最终能成功憋出来
云原生
技术发展迭代不会停歇,没有必要坚持什么“永恒的真理”,旧的原则被打破,只要合理,便是创新。
程序员之路
正视技能收益的意义在于避免自己变得过度浮躁,以“兴趣不合”、“发展不符”为借口去过度挑剔。我也提倡兴趣驱动,提倡快乐工作,但不设前提条件的兴趣驱动就未免太过“凡尔赛”了,首先在社会中务实地生存,不涉及是否快乐,先把本分工作做对做好,再追求兴趣选择和机遇发展,这才是对多数人的最大的公平。
涉及到的技术
zookeeper--> Etcd
配置中心:默认采用Spring Cloud Config ,亦可使用Spring Cloud Consul 、Spring Cloud Alibaba Nacos 代替。
服务发现:默认采用Netflix Eureka ,亦可使用Spring Cloud Consul 、Spring Cloud ZooKeeper 、Etcd 等代替。
服务网关:默认采用Netflix Zuul ,亦可使用Spring Cloud Gateway 代替。
服务治理:默认采用Netflix Hystrix ,亦可使用Sentinel 、Resilience4j 代替。
进程内负载均衡:默认采用Netfilix Ribbon ,亦可使用Spring Cloud Loadbalancer代替。
声明式 HTTP 客户端:默认采用Spring Cloud OpenFeign 。声明式的 HTTP 客户端其实没有找替代品的必要性,如果需要,可考虑Retrofit ,或者使用 RestTemplete 乃至于更底层的OkHTTP 、HTTPClient 以命令式编程来访问,多写一些代码而已了。
单一服务职责、康威定律、自动扩展、领域驱动设计
RPC序列化协议: ONC RPC 的External Data Representation (XDR) CORBA 的Common Data Representation (CDR) Java RMI 的Java Object Serialization Stream Protocol gRPC 的Protocol Buffers Web Service 的XML Serialization 众多轻量级 RPC 支持的JSON Serialization
RPC交换数据协议: Java RMI 的Java Remote Message Protocol (JRMP,也支持RMI-IIOP ) CORBA 的Internet Inter ORB Protocol (IIOP,是 GIOP 协议在 IP 协议上的实现版本) DDS 的Real Time Publish Subscribe Protocol (RTPS) Web Service 的Simple Object Access Protocol (SOAP) 如果要求足够简单,双方都是 HTTP Endpoint,直接使用 HTTP 协议也是可以的(如JSON-RPC)
RPC框架: RMI(Sun/Oracle) Thrift(Facebook/Apache) Dubbo(阿里巴巴/Apache) gRPC(Google) Motan1/2(新浪) Finagle(Twitter) brpc(百度/Apache) .NET Remoting(微软) Arvo(Hadoop) JSON-RPC 2.0(公开规范,JSON-RPC 工作组)
Dubbo协议、 Hessian2作为序列化器
JDBC: java database connection ORM: object relational mapping JMS: java message service
阿里开源的Seata
阿里的 GTS(Global Transaction Service,Seata 由 GTS 开源而来
进程内缓存方案:concurrrentHashMap、Ehcache、Guava Cache、Caffeine
安全框架:Apache Shiro 和Spring Security 。