到目前来看整个架构的演变过程是从单体到水平垂直扩展,再到 SOA 设计,领域驱动设计,事件驱动设计,后到云原生的架构和优势,如果在技术网站甚至微信公众号就可以搜到一堆关于这些的架构设计说明,现在我想换一种方式,从某个点出发,某个概念出发,某个模式出发给大家讲解,然后大家就可以通过这些点的认识扩展到面上甚至,对分布式也有了比较立体的架构思维。
今天主要介绍了不要只顾单体架构的罪孽而忽略了可扩展性这个根本问题,当然分布式架构也有很多问题,也要讲解一下分布式架构面临的主要挑战,也会介绍服务无状态设计给弹性伸缩带来多少好处,一会介绍一下异步通讯的隔离设计,也会有弹性设计的熔断器,这节课没有深刻的技术知识,也没有很多生涩的名词,就是让大家学习如何理解和设计一个分布式架构。我也尽量通俗化的语言进行说明。
现在说一说单体架构,一说到单体架构好像就立马在人堆里找到了犯罪凶手,立马在地上划一道线和单体架构划清界限,单体架构有什么罪呢?我简单给大家罗列了一下,这个所谓的单体架构到底给大家带来多大的麻烦。,功能难以拆分,在说明难以拆分之前我先说一下为什么要实现功能的拆分,我们知道客户提出业务需求,技术按照需求来开发设计功能这个没问题,但是一个系统中分为很多功能的,比如微信的注册登录功能,刷新朋友圈状态功能,提交发布朋友圈功能,给文章点赞功能,甚至发红包抢红包功能,有些功能访问量就很高,比如用户一直在线功能,刷新朋友圈功能,需要高吞吐缓存客户登陆状态,高性能高吞吐量实时获取朋友圈状态,这就造成了功能需要拆分的需求,对吞吐要求高的进行拆分,只有拆出来才可以正确对待,才可以对症下药,才可以正确扩展针对的优化,如何设计一个容易扩展的架构那就是好设计成无状态的应用,后面我会介绍服务的无状态设计。第二,技术难以演进,且说一下技术为啥要演进,比如说你们公司开发了一个客户管理系统,但是以前是使用一个源代码包,后来客户越来越多,技术发展也越来越快,以前简单的 MVC 架构现在需要依赖反转设计,也有了开发更加容易的 Spring 架构,甚至有数据库的缓存技术,异步的消息中间件技术,这些技术红利单体架构很难享用的到的,如果非用不可,技术演进难度会指数级增加,就代码版本维护这一项就很管理,开发人员痛苦的事是什么?就是看别人的代码,而且越到后面理解这一堆代码的难度就指数级增长,这里面重点是业务和技术都是随时变化的,我们要从变化从架构从人的角度上考虑技术的选型以及架构设计,如何面向人的架构设计是一个更高的命题。第三,系统难以维护,单体节点上的软件通常不应该出现模棱两可的现象:比如说,当硬件正常工作时,相同的操作通常会产生相同的结果(即确定性);而如果硬件存在问题(例如,内存损坏或糖口松动),结果往往是系统性故障,如内核崩溃,蓝屏死机,启动失败等。因此在单节点上一个质量合格的软件状态要么是功能正常,要么是完全失效,而不会介于两者之间,但是有这么一个事,我们作为运维维护系统,大部分都是为了面对快要不可用之前预料故障解决故障,在生死边缘让系统正常运作不会影响到前端客户的体验,运维就是要保证即使出错的情况下我们也可以让系统大程度的可用,如果单体节点,那这种运维方式就玩不了,运维的空间就不多,所能做的事情也有限,没有发挥空间的余地,很难维护。这里关于维护的事情多说一句,其实在这个逻辑的背后涉及计算机设计一个非常审慎的选择:如果发生了某种内部错误,我们宁愿让计算机全部崩溃,而不是返回一个错误的结果,错误的结果往往更难处理。因此,计算机隐藏了一切模糊的物理世界,呈现以一个理想化的系统模型,像以数学完美的方式运行。CPU指令通常以确定性方式操作,如果写入一些数据到内存或磁盘,那么这些数据将保持不变且不会被随机破坏。这种确定-正确性的设计目标可以一直追溯到台数字计算机。然而涉及到多台节点时,情况发生了根本性的改变。尤其对于这种分布式系统,理想化的标准正确模型不再适用,我们必须面对一个现实,系统越来越混乱,各种各样的事情都可能出错,后面我会讲解把避免故障隔离故障当做一个功能设计。第四,性能难以扩展,我们接业务需求的时候,我们开发了业务需求的功能,但是非业务需求的功能呢?什么是非业务需求?必须要非业务需求的功能吗?还是那句话业务越来越多,需求越来越细,我们要拆分功能是必要的,比如说注册和登陆功能,首先这两个功能要保证顺序一致性的,而且登陆需要保存客户登陆状态的也就是数据的状态,如果我们扩展用户登录的服务,就需要保证这个服务具有扩展性,但是用户登录的状态如何保存呢,如果实现自动扩展有数据状态的服务,其实无状态的服务容易弹性伸缩,我们要如何保障无状态呢?这些就是非功能需求,这里多说几句,大家知道PaaS吧,PaaS其实很难设计,为什么呢?就是因为我们要再设计系统之初就要保证这些非功能需求的实现,达到这种情况,达到客户可以不用考虑或者感知不到这些非功能设计而让客户达到满意的效果,后面我使用AKF立方体的方法给大家介绍如何达到可扩展性。第五,艰难自我救赎,单体节点也不是完全达不到高可用,那就是模块化的水平扩展,就是部署多个单体模块前面加一个负载的应用对很高的流量进行均衡,但是这种方法有很大的问题首先这个流量高是高在哪里,如果只是恰巧客户在做抢红包活动,只是一个抢红包的功能我们就要水平扩充几十个应用包吗?况且应用包扩容了我们要实现数据库的读写的,数据库都是有状态的,我们如何大限度无感知的扩容,但是这里边我就要说一下 AKF 立方体了,AKF 的这个工具可以帮助大家正确理解这个问题并且找到大家都认同的方法。后,说了这么多的单体架构的罪,单体架构真的无用武之地,不是的,上面的我罗列的几个事项,是因为业务变化,技术变化,市场的变化导致要求越来越高了,如果需求简单,我觉得单体架构的模式还是可以使用的。
说了这么多,你们是不是要问了,我们今天的课程也是和网上的文章一样先贬低单体架构在突出分布式架构吗?其实不是的,我上面给大家罗列了所谓单体架构的罪孽,但是结果是我们依然不知道如何 界定什么是单体架构,但是我们依然想和他永远的划清界限,其实问题的产生都是因为我们在设计架构的时候,没有从扩展性这个根本性的问题考虑,我们说好多单体架构的坏话,但是单体本身没错,错的是我们对架构的理解方式,如何建立一个适应业务变化,技术变化,市场变化的架构呢,如何理解这种具有扩展性的架构呢?,如何设计一个可以扩展的架构?如何让所有人都认同这种扩展模式呢?我这里使用了 AKF 立方体的设计模式,AKF 是什么呢?AKF 扩展立方体(AKF Scale Cube)是一个描述从单体应用到一个分布式可扩展架构的模型概念,他可以完美地从概念上和理解上从单体到分布式架构完美过度,还很容易形成一门普世的架构语言模型,比较容易让所有人理解并且认同。简单来说,立方体,就是一个三维的空间,我们终要表达的是 “可扩展”性,那么没有比三维的空间更能表达扩展性的了。AKF立方体是一种理论指导,它可以帮助我们在各个方向上做扩展。X、Y、Z三个方向都给我们指明了道路,让我们在执行的时候可以在不同的方向上做一个参考,参照这一理论模型来确认我们所做的扩展动作是否符合这一理论,就是提出一个方案,然后公司很多不同职位的人通过 AKF 立方体进行评估。首先我通过举例子的方式给大家说明一下 AKF 立方体的 X 轴,Y轴,Z 轴的设计,AKF 扩展立方体的X轴代表无差别地克隆服务和数据。我们用人和组织是来说明这种分割的方式。让我们先回想一下在过去靠打字小组(typingpool)来处理会议纪要、信件、内部备忘录等的情形。这种打字的工作被送到打字小组,然后几乎无差别地分配给每个打字员。这种工作分配就是一个完美的 X 轴扩展实例。简单来说当我们需要执行更多任务时,只需添加更多的克隆实体就可以了,X 轴看起来似乎很伟大!如果备忘录的数量超过了当前的打字能力,只需添加更多的打字员就 OK 了。但是问题来了,有一个 X 轴就可以了吗?为什么扩展立方体还需要其他两个轴呢?事实情况是,让我们先回到打字小组这个例子来回答这个问题。为了要完成备忘录、对外函件和笔记的打字,不同类型的文件打字员需要某些知识。随着公司的发展,假设打字小组提供的服务量要求增加了,就是打字效率和打字成品完美度提高了。而现在有 100 种不同类型和格式的打字服务,而这些服务的分布并不是均匀的,90% 的工作是需要普通知识的类型,10% 的工作需要别的专业知识的类型。打字员可能会很快完成那些常见的任务,如果需要花些时间查找那些不常见的格式,就会减缓整个任务流水线的速度。这个时候就需要 Y 轴分割了,AKF 扩展立方体的Y轴代表的是按照交易处理的数据类型,交易任务类型或两者的组合进行分割,也就是按照工作职责和服务分割。理解这种分割的一种方法是对行动的责任进行分割。我们从汽车“加工车间”发展到更加专业化的系统来看,正如亨利·福特在汽车制造装配线上所做的那样。与其安排 100 个人制造 100 辆独特的汽车,每人完成 的造车任务,不如让 100 个工人执行子任务,如发动机安装、喷漆、安装挡风玻璃等。按照职位职责分割就是 Y 轴的逻辑,Y 轴分割的好处很多。不但有助于管理,也有助于扩展那些需要掌握某些信息才能处理交易的工作,意味着人员和系统可以更加专业化,从而提高每个人或每个系统的吞吐量。其实这就是一个简单的Y轴分割,也会带给我们简单的故障隔离。这种隔离类似于服务之间的隔离,有些类似 SOA 设计,面向服务设计。扩展立方体的 Z 轴通常基于请求者或者客户的信息进行分割。对打字服务组进行 Z 轴分割时,我们既要了解请求打字的人,也要了解分配到任务的打字员。在分析打字工作请求时,我们可以看看提出独特工作或代表特殊工作量的人群。有些类似 VIP 客户的需求和专家组的工作分割。比较起来其实z轴也具有一定的用户隔离性,就是按照用户使用情况的隔离,这种隔离和架构中的多租户模式非常相近,虽然多租户模式有多种实现方式,对服务和数据不同程度的共享,可能租户之间服务不共享,但是数据共享,也可能相反。但是总的来说相对客户来说是隔离的。总的来说,X 轴代表对服务和数据的复制,Y 轴代表按照职责和服务进行分割,Z 轴代表基于客户或者请求者分割。但是我想说什么呢?这种 AKF 立方体有什么用呢?每个轴都有扩展性的属性,而且每个轴都有益于形成简单的概念化,这些角度的变化更加有助于我们从单体扩展到分布式的理解。这种扩展性是随着我们对技术的理解,对业务的理解,对产品的理解,对流程的理解,对服务的理解是逐步同步加深迭代的,理论上x轴这种工作小组式的扩展在技术上可以无限水平扩展的。但是从业务角度来看,业务更关心应用中的服务是否正确正常高效高质量的运行,单纯 X 轴扩展做不到,这就需要从 Y 轴对服务功能的拆分,对高效率高质量的扩展,而且从 Z轴可以按照现实情况,按照地域要求,按照客户的特殊要求进行扩展,类似的如数据持久化到硬件的扩展,其实大家可以想一下,有 X 轴无差别的扩展,势必影响到 Z 轴要求的特殊情况的扩展,否则 Z 轴就会成为瓶颈,Z 轴扩展的成本可能高一些,但是回报也是很高的。所以技术开发的服务要尽量契合产品业务需求的,迎合使用者的,提高效率的,提高质量的,而且还要处理特殊请求的比如数据存储的状态,从过程上来说无论是大部分可以标准化的或者特殊的都可以尽量满足流程的扩展性的。就是说这里的扩展不只是技术架构的扩展,也是针对业务需求的扩展,也是产品的完善;流程的标准化自动化;平台的高可用性,稳定性以及资源复用和成本管控。简单来说,技术可以使用 AKF 它了解架构的可用性和可靠性;业务使用它可以连接业务的流程和交易价值,产品使用它可以充分了解他的优势和适应性。
按照上面这张图片所示,如何对微服务架构中的拆分 立方体的 X 轴扩展就代表水平复制,通过复制节点,实现多个节点同时提供服务,从而大大提高系统的总体容量、解决单点问题等。比如 nginx 反向代理多个服务节点,实现多个应用的负载和流量均衡,进而扩大吞吐量,但是这种服务的可伸缩性,好把服务设计成无状态的,后面我会讲解无状态,Y 轴扩展从业务层面来考虑扩展方式,因此又称为业务扩展或资源扩展,这点基本上就等同于微服务化。系统从业务层面拆分为多个子系统,各子系统由单独的团队负责整个生命周期的维护,单独部署运行,而且子系统间具备故障隔离的能力,再说一下 Z 轴扩展,Z轴扩展一般用来扩展数据库,把那些优先处理的数据和服务按照多租户的模式进行资源一定程度的共享,更多处理的是数据的状态。(数据库分片)在做某些查询的时候,查询指令被送给每一个分片,然后分别查询,终获得的结果聚合之后返回。大概是这样。从实际情况触发,从 Y 轴扩展按照服务分割,比如登陆是一个服务,注册又是一个服务,但是每个服务中都有些不同程度的重复使用,如果登陆的人数增长了许多,所以 Y 轴扩展中有x轴扩展,就是把登陆服务进行水平扩展,以应对那些增加的用户登录,但是登陆服务要保存服务的状态和数据的状态,如果数据状态缓存在数据库中,这就要需要对数据库读写的扩展,数据库一般是读多写少,为了读取方便可以设置多个分片,有的分片副本作为写,有的作为读,这样的扩展性从技术上来看就比较清晰了,X 轴,Y 轴,Z 轴都有了关联性的联动的扩展。X 轴偏技术考虑,Y 轴偏业务考虑,Z 轴偏现实特殊情况考虑,无论从业务还是技术角度去看,都可以理解 AKF 的所要表达的每个轴的独立扩展性和多个轴的共同扩展的相关联性,技术和业务可以独自沿着自己的道路迭代扩展,也可以在考虑现实的情况下,业务和技术2条腿的方式交替前行,交替扩展。但是 AKF 这个设计除了解决扩展性问题还有一个用处,同时也减少了变更的风险。如何说清楚这个变更的风险呢?大家都知道过程吧,美国人很喜欢自动化的,比如说亚马逊 AWS 云计算有几千万的服务器但是整个云计算才有2万人不到,这个意味着什么呢?意味着一人平均管理几千服务器,但是他们很喜欢搞自动化,全部尽量自动化来管理这些机器,如何自动化呢,他们认为能自动化的就可以结构化,可以结构化的就必须说的清楚整个过程,并且形成标准化,有了清楚的过程和标准的过程意味着什么,意味着你遇到问题可以不用请示别人甚至领导,可以不用自己主观判断直接按照标准执行就可以了,所以过程一定要清晰这是标准化结构化自动化的要求。如果有一个新的过程,可以把它交给多个开发团队中的某一个开发团队,然后分析这个过程,确定由这个团队中的哪个功能职责部门负责,或者某个专门的部门负责。你就可以看到这个新的过程是如何与有限的一群人一起工作的,然后进行度量,以决定是否把它推广到其他的团队,当然在推广之前要不断的修改和优化。我们就使用 AKF 立方体模式去规避过程的风险,它可以作为任何围绕可扩展性话题进行讨论,AKF 作为这个讨论的基础,因为它有助于在一个组织中创造一种工程师之间的共同语言,无论是技术还是业务。但是不是谈论特定的方法,团队可以专注于概念,而且X 轴,Y 轴,Z 轴无论我们如何选择都是很容易概念化的,这样就很容易在很多人中形成一个共识的理论基础,并可能由此演变成任意数量的方法。进而尽量去定义这个过程,并且可以形成标准推广出去,这就是 AKF 的重要的作用。说了这么多,前面我们讲解了单体架构,后面那就是分布式架构了,我想说的对于扩展性的理解而言使用 AKF 的方式更加具有说服性,对扩展性的表达和描述更有力度。但是 AKF 设计只是一个可扩展模型的概念,有什么问题呢?容易造成为了扩展而扩展,而忽略了分布式架构的问题和挑战,因为我们在真正使用的过程中我们会遇到很多故障,有些故障直接导致服务的全面失效导致终的不可用,这就很严重了,所以我们要尽可能把所有故障都要预料到并且有效的避免或者减轻故障,后可以尽快的恢复服务正常运行,无论是什么故障我们的目标还是一致的,那就是尽快恢复系统。
再说如何面对故障的设计,如何面对恢复的设计之前先说一下无状态的一些知识,前面说了按照 X 轴扩展好是设计成无状态的服务。什么是无状态服务?这里我详细说一下,一个应用或者协议都不使用状态称为无状态应用(stateless),http 就是一个无状态协议,因为他不需要请求之前的任何信息他就知道如何满足下一个请求的一切信息,用户登录的会话机制如果保存在后台中,就是一个有状态的应用,这种有状态应用可以使用户和服务器之间保持一种非常紧密的关系,因为只有这台服务器保存了用户会话信息,很难通过水平扩展服务器来做高可用,所以无状态本质上就是相同的用户行为在后端关闭原来的服务进程,而新启动的服务进程也可以达到相同的效果而前端无感知,作为一个具有扩展性的架构,无状态很容易达到弹性扩缩容的。无状态的服务其实和这个“函数式编程”的思维方式如出一辙。在函数式编程中,一个铁律是,函数是无状态的。换句话说,函数是 immutable 不变的,所有的函数只描述其逻辑和算法,根本不保存数据,也不会修改输入的数据,而是把计算好的结果返回出去,但是,现实世界是一定会有状态的。那如何设计在这些有状态的架构呢?为了做出无状态的服务,我们通常需要把状态保存到其他的地方。比如,不太重要的数据可以放到 Redis 中,重要的数据可以放到 MySQL 中,或是像 ZooKeeper/Etcd 这样的高可用的强一致性的存储中,或是分布式文件系统中。于是,我们为了做成无状态的服务,会导致这些服务需要耦合第三方有状态的存储服务。这就造成了一方面是有依赖,另一方面也增加了网络开销,导致服务的响应时间也会变慢。所以,第三方的这些存储服务也必须要做成高可用高扩展的方式。而且,为了减少网络开销,还需要在无状态的服务中增加缓存机制。然而,下次这个用户的请求并不一定会在同一台机器,所以,这个缓存会在所有的机器上都创建,你看有状态的应用处理起来很麻烦的。但是终这些方案大部分将服务和存储分离,也是为了让自己的系统更有弹力。服务做到无状态高扩展性了,那么数据的存储节点呢?只要有数据持久化就会有状态,如何把数据存储做到具有很强的扩展性,要解决数据结点的 Scale 问题,也就是让数据服务可以像无状态的服务一样在不同的机器上进行调度,这就会涉及数据的 replication 问题就是数据的副本分片。而数据 replication 则会带来数据一致性的问题,因为副本意味着用户保存一份数据数据库就要保存多份的副本,这就需要保证多个副本时候可能要保障数据一致性和时间一致性,做不到就可能数据丢失,进而对性能带来严重的影响。说实话,要解决数据不丢失的问题,只能通过数据冗余的方法,就算是数据分区,一份数据分成多份,每个区也需要进行数据冗余多个副本处理,这就是数据副本。当出现某个节点的数据丢失时,可以从副本读到。数据副本是分布式系统解决数据丢失异常的手段。总的来说:要想让数据有高可用性,就得写多份数据。写多份会引起数据一致性的问题。数据一致性的问题又会引发性能问题。在解决数据副本间的一致性问题时,就有分布式事务各种解决方案,太乱了一切都变成了架构师的trade-off。这里多说几句,也有人说真正完整解决数据扩展性问题的应该还是数据结点自身。只有数据结点自身解决了这个问题,才能做到对上层业务层的透明,就是说业务层可以像操作单机数据库一样来操作分布式数据库,这样才能做到整个分布式服务架构的调度。也就是说,这个问题应该解决在数据存储方,业务层不管这个事该保存的保存,该查询的查询就像操作单机数据库一样。但是因为数据存储结果有太多不同的 Schame(数据库对象),所以现在的数据存储对象也是多种多样的,有文件系统,有对象型的,有 Key-Value 式,有时序的,有搜索型的,有关系型的…这就是为什么分布式数据存储系统比较难做,因为很难做出来一个放之四海皆准的方案。所以,真正解决数据结点高可用高性能稳定性的方案应该是底层的数据结点。在它们底层上面做这个事才是真正有效和优雅的。我相信状态数据调度应该是在 IaaS 层的数据存储解决的问题,而不是在 PaaS 层或者 SaaS 层来解决的,就是说,我们只需要一个底层是分布式的文件系统,增加新的结点只需要做一个简单的远程文件系统的 mount 就可以把数据调度到另外一台机器上了,这样就做到了上面说的服务和存储的分离。
接下来我给大家讲解一下面向故障的设计,面向失效的设计。一个的架构师通常都是一个悲观主义者,除了能设计好能够支撑业务持续发展的优雅架构,另一个容易被忽略的重要能力在于充分考虑失败的场景可能产生故障的场景。如果对失败场景考虑不够充分,轻则出现业务不可用,影响用户体验和企业声誉;重则导致数据丢失、业务再无恢复可能。在分布式、云架构的互联网时代:失败将由小概率偶发事件变成常态,同时应对和处理失败的具体实现方式和之前也大相径庭,就是前面说的我们必须面对一个混乱的现实,就是各种各样的事情都可能出错。那现在就简单给大家介绍一下无所不在的容易失败导致故障的场景。互联网业务快速发展不仅直接带来了流量、安全等不确定性,同时促使了技术架构的快速演进,使架构变得越来越复杂,这些因素都将导致故障发生的概率大幅提升。问题是当人类的工作、生活越来越依赖互联网,一旦出现故障,造成的影响和损失将是空前巨大的。比如说在远古时代,人类没有自来水也没有电,一切都很好;今天如果停电停水一段时间,相信很多人都会无法适应。而互联网正在逐步演变成跟水和电一样的基础设施,所以出现了故障无论是对于客户还是公司都是一件很严重的事件。所以只有意识到故障随时可能发生,才能设计出有效应对。现在给大家说一下容易导致故障的场景:硬件问题:首先硬件是有生命周期的,它一定会老化,并且你不知道它会在什么时候坏;其次硬件是一个实体,它存在于客观环境当中,它的状态会受外部环境干扰,比如火灾、地震等外力因素都可能导致硬件损坏;后所有硬件都会存在残次品,你很可能就是那个不幸者。通常情况下单个硬件出问题的概率不高,但是当有几十万的硬件设备,硬件的失败问题每天都会发生。软件bug:即便是程序员写出来的程序,经过测试同学的严格测试后的代码,上线依然无法做到完全没有 bug。互联网业务迭代往往讲究一个“快”字,而且以往几个月或者几年升级一次的软件程序,现在一周就需要升级一次或者多次,这大幅提升了软件出错的可能性。配置变更错误:系统运行态的日常运维过程当中,难免会因为疏忽或者考虑不周全导致灾难。哪怕是 6 个 9 的可靠性,依旧无法做到万无一失。全局的流量入口、权限与安全验证体系、统一网关与接口平台等技术环节是可能促发全站不可用的重要风险点,对于影响面大的配置的变更需要尤为谨慎。系统恶化:原本工作地很好的程序随着时间的推移可能有一天不再正常工作,举几个常见的例子:自增变量运行了很长一段时间后出现越界、缓存随着数据量的逐渐变大而出现空间不足、数据库连接池随着机器的扩容而不够用等等。千万不要认为运行良好的系统是不会出问题的,它的代码里面可能藏了定时*炸*弹,只是你不知道会在什么时间点爆炸。超预期流量:某一天你的系统可能突然会承受远超过预期的每秒请求数,特别是在“中国特色”的互联网场景之下,你很难预估系统各个时间点的业务访问量。外部攻击:你需要考虑各种攻击行为,包含流量攻击和安全攻击。你的系统可能随时会面临着 DDOS 和 CC 类攻击,你传输的数据可能会被盗取或者篡改。依赖库问题:你的系统很可能会用大量的二方库或者三方库,它们对你来说是黑盒子,你不了解它们存在哪些风险,并且你无法掌控。这些库可能会存在漏洞、可能会有 bug,可能会大量消耗你的系统资源,总之不要太信任它们,就像近的 log4j 漏洞一样,相信很多公司日志框架设计都使用 log4j,有漏洞就要下载新的安全包,然后在测试环境部署测试,后部署生产,所以很麻烦。依赖服务问题:你依赖的服务也一定不会 可用,它们可能会超时,可能会失败。当依赖服务超时的时候,如果你没有很好的处理,可能会导致你自己的系统无法工作,在分布式场景下,这种失败状态会持续辐射,终导致大面积的不可用。你依赖的服务也一定不会 可用,它们可能会超时,可能会失败。当依赖服务超时的时候,如果你没有很好的处理,可能会导致你自己的系统无法工作,在分布式场景下,这种失败状态会持续辐射,终导致大面积的不可用。
作为一个悲观主义者,你需要在一开始的系统设计阶段就考虑到以上各种失败场景,把面向失败当成系统设计的一部分,并且准备好从失败中恢复的策略,这有助于更好地提升整个系统的可用性。只有你意识到事情会随着时间的推移而失败,并将这种思想融入到体系结构中,那么在失败发生的时候你才能完全不受影响或者将失败损失降到低。面向失败的设计理念数十年来并没有多大的变化,一些好的经典原则在今天依旧被广泛运用。大量的系统故障是因为人的失误造成的,即便让一个的运维工程师进行一万次同样的运维操作也难免不出错。的解决办法便是在运维过程当中尽可能降低人为操作的比重。系统化、白屏化是个阶段——将人为的操作步骤固化成系统程序,避免操作失误;自动化是第二个阶段——将正确的决策过程也固化成智能程序,避免决策失误。同时所有的运维动作都需要遵循灰度原则,即便出现了失误也能控制好爆炸半径。如何来衡量一个软件系统的设计是否优良?一条很重要的衡量标准——在任何情况之下你的软件系统都应该工作在当前环境的优状态。每个人都知道机翼是飞机的重要部件,一旦机翼出现问题,飞机很可能就会坠落。然而在二战当中,许多战斗机即便机翼千疮百孔了,依然保持着佳战斗能力;甚至还有更夸张的情况:1983年的一次战斗机演习当中,一架飞机由于事故损失了一个机翼,这架缺少一个机翼的飞机依然保持了飞行能力、终完成安全着陆。软件系统由两部分构成:系统自身的代码和依赖的库以及服务。“服务能力与依赖调用自我保护”需要从这两块分别切入构建系统,就是说在任意情况都始终工作在佳状态的能力。服务限流、系统负载保护、给依赖的服务设置超时或者资源限制等都是相应的应对策略。硬件和软件都不可靠,环境和人都存在极大的不确定性,虽然无法避免失败场景的发生,但是可以通过冗余设计来规避局部失败对系统的影响。冗余设计避免单点故障这一策略在互联网技术架构中处处可见,比如重要的服务通常都会部署多个、数据库的主备结构、服务调用的重试机制、存储的多副本等概念都属于这一范畴。除了局部失败场景,你的系统可能还面临着大范围的失败场景。大范围的原因有两个:1、天灾,比如火灾、地震、台风、雷电等大的自然灾害可能导致大面积的基础设备被毁坏;2、人祸:人的失误或者刻意破坏行为有时候也会酿成大祸,如操作错误、 破坏、植入有害代码和恐怖袭击。“面向失败的宏观多活架构”从宏观架构的高可用层面来解决系统的整体可用性问题,随着技术的演进,冷备、热备、两地三中心、异地多活等应对大范围失败场景的技术体系这些年频频被提起。故障与攻防演练锤炼容灾应急能力---其实就是网上说的混沌工程后,即便以上工作都做好了,你也不能高枕无忧去等待失败的到来。你的设计、系统、流程、技术人员等需要通过不断的演练来保障能力和进化升级。对于代价非常巨大的事件,做好前期的充分演练是非常有必要的,比如军事演练、消防演练等都属于这一范畴。而系统不可用的代价对于企业来讲很可能是无法承受的,因此需要在平时做好充分的演练:通过故障与攻防演练锤炼容灾应急能力,对面向失败的设计做好充分验证。只有当所有的失败场景都被提前演练过,当失败真正来临时才能做到胸有成竹。
前面所说的隔离设计通常都需要对系统做解耦设计,就像是y轴分割一样,而把一个单体系统解耦,不单单是把业务功能拆分出来,拆分完后还会面对很多的问题。其中一个重要的问题就是这些系统间的通讯。系统间的通讯分为同步方式和异步方式,同步通讯就像打电话,需要实时响应,你说一句我说一句,你还没说完我就等待,而异步通讯就像发邮件,不需要马上回复。在面对超高吐吞量的场景下,异步处理就比同步处理有比较大的优势了,这就好像一个人不可能同时接打很多电话,但是他可以同时接收很多的电子邮件一样。这里我给大家详细说一下异步的好处,先说一下同步的劣势:同步调用需要被调用方的吞吐不低于调用方的吞吐;同步调用会导致调用方一直在等待被调用方完成,如果一层接一层地同步调用下去,所有的参与方会有相同的等待时间;步调用只能是一对一的,很难做到一对多;同步调用不好的是,如果被调用方有问题,那么其调用方就会跟着出问题,于是会出现多米诺骨牌效应,故障一下就蔓延开来。一言以蔽之同步通讯有互斥性而且依赖强。简单来说为什么要消除同步调用呢?举一个更简单的例子,还记得那些的圣诞树上的灯串吗。如果这种灯串中的一个灯泡熄火,会导致灯串中其他所有的灯泡熄火。这些灯泡是串联的,所以任何一个灯泡出现故障,整个灯串都会失效。因此灯串的可用性是所有灯泡可用性的乘积。如果任何一个灯泡的可用性是 99.999%或者失效概率为 0.001%,灯串中有 100 个灯泡,那么灯串的理论可用性为 0.999,从5个9的可用减少到3个9的可用性(可用性百分比中有多少个9)。如果这些灯保持开启一年时间,在一年的时间里,5个9的可用性,99.999%有刚刚超过五分钟的停机时间(即灯泡坏了),而一个3个9的可用性99.9%有超过500分钟的停机时间。看着就是同步的坏处。异步通讯相对于同步通讯来说,有很多好处,除了可以增加系统的吞吐量之外,大的一个好处是其可以让服务间的解耦更为彻底,如何解耦的,就是让系统的调用方和被调用方可以按照自己的速率而不是步调一致,从而可以更好地保护系统,让系统更有弹力。异步通讯的本质就是发送方只管发送不管接收方是否收到信息,而接收方可以更加的主动和随意接收信息,这样就让服务间的解耦更为彻底,就像是客户下单一样以前是客户下单之后直接访问商店的库存然后返回,但是现在是客户下单放在消息队列中就成功返回,而商店老板在去消息队列中订阅数据,所以解耦的目的是啥?解耦的目的是让各个服务的隔离性更好,这样不会出现“一倒倒一片”的故障。异步通讯的架构可以获得更大的吞吐量,而且各个服务间的性能不受干扰相对独立。现在给大家讲解一个异步通讯常用的方式就是通过 Broker 通讯的方式。所谓 Broker,就是一个中间人,发送方(sender)和接收方(receiver)都互相看不到对方,它们看得到的是一个 Broker,发送方向 Broker 发送消息,接收方向 Broker 订阅消息。其实就是像kafka。这是完全的解耦。所有的服务都不需要相互依赖,而是依赖于一个中间件 Broker。这个 Broker 是一个像数据总线一样的东西,所有的服务要接收数据和发送数据都发到这个总线上,这个总线就像协议一样,让服务间的通讯变得标准和可控。在 Broker 这种模式下,发送方的服务和接收方的服务大程度地彻底的解耦。利用 Broker 或队列的方式还可以达到把抖动的吞吐量变成均匀的吞吐量,这就是所谓的“削峰”,就是说客户的订单突然多了也不会影响后台商店的订阅,因为商店可以保持自己的步调自由的逐步的去消费里面的数据而不会对后台系统造成故障,所以这对后端系统是个不错的保护。但是所有人都依赖于一个总线,但是这个总线就需要有如下的特性:必须是高可用的,因为它成了整个系统的关键;必须是高性能而且是可以水平扩展的;必须是可以持久化不丢数据的。说到了 kafka 发布订阅机制,在趁热打铁说一下事件驱动设计(Event-Driven Architecture,EDA)吧。其实从需求分析和建模的软件开发历史来看我们对业务操作更感兴趣,因为有业务操作才能串联多个领域上下文,盘活整个模型。事件驱动让人们看到了业务操作带来的后置变化的价值,类似于因果关系。更重要的是某件事情发生我们希望明确的知道这件事会引起什么其他事情发生。那么对于事件而言,感观上就是事情发生后的事件,但是扩展一点也可以代表事件本身,相当于语境不同这个词也有不同的指向,比如说同样是客户买的商品,但是这个商品对于商店来讲是货品,对于生产者来讲是产品,对于销售来讲是某种服务,都是一个东西但是在不同语境就有不同的指向。回到事件本身,我们更容易对业务流程中的多个节点通过事件表达,这让我们可以从繁杂的领域模型里暂时解脱出来看业务本身的上下文关系。所以事件驱动本身其实也代表着业务流程的本身,只是做了一些精简,这像是一种假象暂时让我们觉得事件驱动就是可以直面业务的,可以方便容易建模的。在 DDD 中也一样,事件驱动架构风格和事件溯源模式弥补了 DDD 面向微服务和分布式的一个短板。DDD 本身的领域事件可以在事件驱动架构中进行整合,具体到项目里就是事件消息。事件驱动架构通过消息来构建上下游之间的关系。这种关系不是直接能看到的,就比如下游到上游的消息,上游发的消息哪些下游能收到。事实上,这种模式解决了两个问题,一个是上下游之间的服务依赖耦合,另一个是对整个业务逻辑的补充。就像上面说的 Broker 异步通讯模式。事件驱动好是使用 Broker 方式,服务间通过交换消息来完成交流和整个流程的驱动,而这个信息好封装成一个事件。我们说一下一个订单处理流程。客户在网页生成一个单子但是还没支付,就是刚刚点击了下单还没点击支付,这个时候下单服务通知订单服务有订单要处理,而订单服务生成订单后发出通知,库存服务和支付服务得到通知后,一边是占住库存,另一边是让用户支付,等待用户支付完成后通知配送服务进行商品配送,这里边的通知都是异步方式的。这里的下单事件,订单生成事件,付款事件,就是我们所说的事件驱动设计,简单来说在服务之间传递的就是封装好的事件,事件他代表一个语义,代表服务之间的边界,这种其实也类似于面向对象的设计,拥有自包含的特性,所谓“自包含”也就是没有和别人产生依赖。而要把整个流程给串联起来,我们需要一系列的“消息通道(Channel)”,而事件就是串联服务的那根线,比如下单事件串联了下单服务和订单服务。总之就是各个服务做完自己的事后,发出相应的事件,而又有一些服务在订阅着某些事件来异步联动走完整个过程。服务间的依赖没有了,服务间是平等的,每个服务都是高度可重用并可被替换的。服务的开发、测试、运维,以及故障处理都是高度隔离的。服务间通过事件关联,所以服务间是不会相互 block 的。在服务间增加一些Adapter(如日志、认证、版本、限流、降级、熔断等)相当容易。服务间的吞吐也被解开了,各个服务可以按照自己的处理速度处理。服务相对独立,在部署、扩容和运维上都可以做到独立不受其他服务的干扰。我们知道任何设计都有好有不好的方式。事件驱动的架构也会有一些不好的地方。业务流程不再那么明显和好管理。整个架构变得比较复杂。解决这个问题需要有一些可视化的工具来呈现整体业务流程。事件可能会乱序。这会带来非常 Bug 的事。解决这个问题需要很好地管理一个状态机的控制。事务处理变得复杂。需要使用两阶段提交来做强一致性,或是退缩到终一致性。
上文说过:作为一个悲观主义者,你需要在一开始的系统设计阶段就考虑到各种失败导致故障的场景,把面向故障当成系统设计的一部分,并且准备好从故障中恢复的策略,因为这有助于更好地提升整个系统的可用性。现在我们好好谈一谈面向恢复的设计,我们了解到故障是影响,我们解决故障的时候没必要找到原因,而我们要解决的是减少影响尽快满足系统大可用性,发生了故障首先重要的是我们要解决的问题是如何尽快恢复系统正常运行。不可预测的故障就如不可预测的线路,我们无法预测到底哪条路线出问题,出什么样的问题,但是我们可以凭借经验通过监控的数据可以预测到可能的故障,监控就像我们的眼睛,没有他,我们就不知道系统运行状态以及故障和行为的预测,也不可能运维自动化。我们可以通过系统的体检和急诊判断异常,体检就是资源和性能的全局监测,从容量管理监测。提供一个全局的系统运行时数据的展示,可以让工程师团队知道是否需要增加机器或者其它资源。从性能管理监测。可以通过查看大盘,找到系统瓶颈,并有针对性地优化系统和相应代码。急诊是什么,急诊就是已经出现了问题,为了避免故障多米若骨牌效应,故障连锁效应,实现快速定位问题,可以快速地暴露并找到问题的发生点,帮助技术人员诊断问题。并且对别的有关联的服务进行性能分析,并且当出现非预期的流量提升时,可以快速地找到系统的瓶颈,并帮助开发人员深入代码。服务调用链跟踪。这个监控系统应该从对外的API开始,然后将后台的实际服务给关联起来,然后再进一步将这个服务的依赖服务关联起来,直到后一个服务,这样就可以把整个系统的服务全部都串连起来了。服务调用时长分布。可以看到一个服务调用链上的时间分布,这样有助于我们知道耗时的服务是什么。服务的 TOP N 视图。所谓 TOP N 视图就是一个系统请求的排名情况。一般来说,这个排名会有三种排名的方法:a)按调用量排名,b) 按请求耗时排名,c)按热点排名(一个时间段内的请求次数的响应时间和)。数据库操作关联。对于 Java 应用,我们可以很方便地通过 JavaAgent 字节码注入技术拿到 JDBC 执行数据库操作的执行时间。对此,我们可以和相关的请求对应起来。服务资源跟踪。我们的服务可能运行在物理机上,也可能运行在虚拟机里,还可能运行在一个 Docker 的容器里,Docker 容器又运行在物理机或是虚拟机上。我们需要把服务运行的机器节点上的数据(如CPU、MEM、I/O、DISK、NETWORK)关联起来。这样一来,我们就可以知道服务和基础层资源的关系,通过多条线定位故障点。如果是 Java 应用,我们还要和JVM里的东西进行关联,这样我们才能知道服务所运行的JVM中的情况。当一台机器挂掉是因为 CPU 或 I/O 过高的时候,我们马上可以知道其会影响到哪些对外服务的API。当一个服务响应过慢的时候,我们马上能关联出来是否在做JavaGC,或是其所在的计算结点上是否有资源不足的情况,或是依赖的服务是否出现了问题。当发现一个 SQL 操作过慢的时候,我们能马上知道其会影响哪个对外服务的 API。当发现一个消息队列拥塞的时候,我们能马上知道其会影响哪些对外服务的 API。总之,我们就是想知道用户访问哪些请求会出现问题,这对于我们了解故障的影响面非常有帮助。一旦发现某个服务过慢是因为 CPU 使用过多,我们就可以做弹性伸缩。一旦发现某个服务过慢是因为 MySQL 出现了一个慢查询,我们就无法在应用层上做弹性伸缩,只能做流量限制,或是降级操作了或者优雅关闭。什么叫做正确的优雅的关闭呢?分布式架构的故障发生率一定比单体架构少吗?分布式架构的故障好恢复吗?这是个很重要很现实的问题,我来回答你,分布式架构的故障一点不比单体架构少,而且很严重的时候,一个故障会连带多个故障形成多米若骨牌效应,而且故障发生不可拍,可怕的是故障地位故障的恢复。一个线上故障的工单会在不同的服务和不同的团队中转过来转过去。每个团队都可能成为一个潜在的 DDoS 攻击者,除非每个服务都要做好配额和限流。所以就有了开头的话,优雅的关闭设计方式可以避免故障范围的扩大,可以避免更多的无用功导致资源的浪费,我们都知道重试吧,但是我们要知道重试是有条件的当我们认为这个故障是暂时的,而不是的,所以,我们会去重试。我认为,设计重试时,我们需要定义出什么情况下需要重试,例如,调用超时、被调用端返回了某种可以重试的错误(如繁忙中、流控中、维护中、资源不足等)。而对于一些别的错误,则好不要重试,比如:业务级的错误(如没有权限、或是非法数据等错误),技术上的错误(如:HTTP 的 503 等,这种原因可能是触发了代码的 bug,重试下去没有意义)。就算允许重试也是有时间和次数的限制,还要考虑幂等性,执行多次和执行一次的效果是一样的。和分布式事务,保证原子性所以有事务回滚和事务补偿,还有熔断就是断路器设置,重试失败就直接自己过段断开等待修复,实在不行那就实行服务的降级,忽略发生故障的服务。说了上面这些想不到的故障和异常发生时候,我们好也有预案,尽量把处理的过程详细化 清晰化,让大家都看得董,不用问别人,不用靠自己猜。
熔断器可以使用状态机来实现,内部模拟以下几种状态。闭合(Closed)状态:我们需要一个调用失败的计数器,如果调用失败,则使失败次数加 1。如果近失败次数超过了在给定时间内允许失败的阈值,则切换到断开 (Open) 状态。此时开启了一个超时时钟,当该时钟超过了该时间,则切换到半断开(Half-Open)状态。该超时时间的设定是给了系统一次机会来修正导致调用失败的错误,以回到正常工作的状态。在 Closed 状态下,错误计数器是基于时间的。在特定的时间间隔内会自动重置。这能够防止由于某次的偶然错误导致熔断器进入断开状态。也可以基于连续失败的次数。断开(Open)状态:在该状态下,对应用程序的请求会立即返回错误响应,而不调用后端的服务。这样也许比较粗暴,有些时候,我们可以cache住上次成功请求,直接返回缓存(当然,这个缓存放在本地内存就好了),如果没有缓存再返回错误(缓存的机制好用在全站一样的数据,而不是用在不同的用户间不同的数据,因为后者需要缓存的数据有可能会很多)。半开(Half-Open)状态:允许应用程序一定数量的请求去调用服务。如果这些请求对服务的调用成功,那么可以认为之前导致调用失败的错误已经修正,此时熔断器切换到闭合状态,同时将错误计数器重置。如果这一定数量的请求有调用失败的情况,则认为导致之前调用失败的问题仍然存在,熔断器切回到断开状态,然后重置计时器来给系统一定的时间来修正错误。半断开状态能够有效防止正在恢复中的服务被突然而来的大量请求再次拖垮。实现熔断器模式使得系统更加稳定和有弹性,在系统从错误中恢复的时候提供稳定性,并且减少了错误对系统性能的影响。它快速地拒绝那些有可能导致错误的服务调用,而不会去等待操作超时或者永远不返回结果来提高系统的响应时间。如果熔断器设计模式在每次状态切换的时候会发出一个事件,这种信息可以用来监控服务的运行状态,能够通知管理员在熔断器切换到断开状态时进行处理。
说了这么多,除了让大家不要因为对单体架构的偏见而忘记扩展性的问题,对扩展性的设计可以有多视角多角度多维度的扩展,无状态应用设计和异步的事件驱动方式给给分布式设计的便利太多太多了,大家自己体会。还有就是作为一个高大尚的分布式架构其实也有很多麻烦要处理,我们不得不把故障当做一个常见的现象,一个常见的功能来设计,而我们好把服务尽快恢复正常运行当做一个高可用的重要设计模式。