所谓“当局者迷,旁观者清”,当我迷惑于当前知识的时候,证明我正平行于或低于该知识平面高度去学习这些知识,结果只有一个——“混乱”。因为自身没有一个高层次的“纲”让自己清晰且逻辑地“收编”和“汇总”这些知识,也许“知识焦虑”就这么来的。在管理层面上,面对矩阵型组织架构上X轴(项目经理)与Y轴(职能经理)平面上的矛盾,我会通过高层次的“目标共性”来一致性项目经理和职能经理的共同目标,让“矛盾”转化“合作”,但面对“知识学习”的时候,我却忽略知识的“结构性”问题。所以每当自己“情绪”萌生之前,我会立刻让自己冷静片刻,尽量让自己理性地分析对象的抽象所在。例如面对“微服务”一大堆知识焦虑的时候,我知道自身缺乏了对“微服务知识”管理和汇总的“纲”,而我目前思考得出的这个纲就是对“分布式”的一个系统性认知。
为什么要“分”?
分布式的重点在于“分”,而“分”就是人类普遍的正常性思维。当我们面对一个复杂或者庞大的问题是,我们习惯性地会把它分而治之。这种“分”的技巧贯穿于我们的日常之中,如“大事化小,小事化无”。如下图所示:
“分”之前首先要做的是“识别问题”,这个问题的复杂度到底需不需要分解,就算分到底需要分解到什么程度,就像我们架构一个应用系统的时候,我们会依据各种边界条件去考虑要不要把系统分解,如何分解等。根据各种经验汇总,分布式系统主要有两方面的原因:
增大系统容量(业务复杂度);
加强系统可用(用户量增长);
“分” 和“不分”有什么不同?
活到现在,我觉得这个世界上最有“哲理”的一个词叫“权衡(trade-off)”。任何事情总有两面,从道教学角度看,这叫“取舍之道”,从经济学角度看,这叫“机会成本”。不同选择肯定会带来不同的成本,首先我们得会识别问题,然后从结果成本反推方案选择。例如面对“分”和“不分”时,我们先来看看这两种方案的优和劣:
以上分析都是一些“通用维度”,不同问题和场景可能还会有其他的对比维度。但我觉得仅仅从文字分析可能还不够直观,如果我尝试把“分”和“不分”的问题从“二维”换成“三维”可能会更加直观和深入。
从“二维角度”只看到系统的“平分”,真正的分布式是存在“横向”和“竖向”分离的事实,从以上“立体维度”可以让我们更加清晰地认识到分布式相对单体的复杂并在未来的实践中所需要面临的各种各样的问题需要去权衡。
分布式系统的发展历史
分布式系统是基于SOA(面向服务架构)方法的一种架构方式,从20世纪70年代流行模块化编程。80年代流行面向事件设计,90年代开始流行基于接口/构件设计(SOA方法的起始)。回顾历史有助于我们全局视野的构建,接下来看看分布式系统的三种不同的SAO架构模式:
Pre-SOA(1990s):20世纪90年代的时候,各个服务模块之间是直接相互调用的,从而造成了服务与服务之间的紧耦合问题;
Traditional SOA:21世纪初期为了解决服务紧耦合的问题,各大IT厂商视图通过“中间件”把服务之间的关联解耦(负责路由、负载、协议转换等),如ESB,但这个中间件会显得过于臃肿和集中。
Microservice:从2010年开始,为了让架构更加轻量,出现了微服务架构,把系统根据业务领域分解成各个独立的服务,并且数据库服务等垂直组件跟着分散到不同的服务当中,做到真正的服务独自运行。从上图可以看到,跟传统SOA的不同在于服务之间的整合需要一个服务编排或服务整合“组件引擎”来组织业务。
分布式系统的问题总结
从分布式架构的发展历程可以看到,系统架构会随着时间和发展不断涌现出新的问题,从而去触发系统架构模式的演变。经过了这么多年下来的经验和实践积累,前辈们大概总结出了分布式系统在技术上大概需要注意的问题有四大类:
① 异构系统不标准问题:不同系统之间的架构不统一,协议不统一,语言不统一等问题;
② 系统服务依赖性问题:服务调用链过长,多米诺骨牌效应,关键服务的识别等问题;
③ 系统故障的概率问题:随着资源的增加,如何确保故障时长以及故障影响面积问题;
④ 多层架构的运维问题:如何确保基础层(LaaS)、平台层(PaaS)、应用层(SaaS)以及接入层的问题统一运维问题;
分布式系统的关键技术
所谓“问题触发需求”,如果站在以上“问题”的角度出发,可能更能让自己从高维度了解、分析和把握我们所面对的各种各样的开源技术和工具(如K8S,Docker、Spring Cloud等),而不至于产生无从下手的“焦虑”。很多时候,我们往往会被“分”出来的问题所面对的技术细节给迷惑了,而忽略了“分”之前的整体认知。缺少了对问题整体性的认知,我们是无法体会到基础性知识的魅力和存在价值。面对以上4大类分布式系统问题,前辈们同样总结出了以下4大块分布式系统的关键技术点:
① 应用整体监控:包括基础层监控、平台(中间件)层监控、应用层监控(包括客户端);
② 资源/服务调度:包括计算资源调度、服务调度、架构调度;
③ 状态/数据调度:包括数据可用性、数据一致性、数据分布式;
④ 流量调度:包括服务治理、流量管控、流量管理;
作为技术人员,真心觉得自己非常容易陷入“钉子思维”,主要是自身缺乏这个抽象与整体的视野高度而深陷于技术细节当中,不是说技术细节没用,只不过连一个“主要解决什么问题”的总体性认知都没有,技术细节会更难以理解,焦虑感油然而生。
关键技术一:应用整体监控
以上提到了监控的层面主要包括基础层、平台层、应用层的三个层面,对于一个分布式系统监控工具来说,至少包含全栈监控、(各层面)关联分析、实时告警与自动化运维以及系统性能分析等功能。一个完整的监控系统大概如下:
监控本身就是一件向“可控”看齐的事情,但如果缺乏一定的总体性认知(例如技能分工型组织),该运维小组可能会以监控数据量多为考虑方向(指标),那可能会本末倒置地把原本做简单的事情复杂化,毕竟监控数量太多等于没有数据一样,不知从何入手,所以监控还必须具备一定的大局视野。例如整个系统架构的分布地图,每个分布点(地图)的关键程度以及每个业务流程的走向(服务调用链)等全局信息,才能聚合各种关键监控指标,达到一个整体监控的宏观效果。并且通过整个业务的拓扑关系快速定位异常和告警,进入更深层次的监控明细层面去排除问题。
■项目总结
作为一个从0到1到并持续做了八年(7*24小时运行)的应用系统,在运维层面感受最深的就是监控数据量从单体应用(两台刀片机)到分布式(微服务)应用(数千万客户端、上千个应用节点、数百个Redis实例,并对接N个外围系统等)的指数增长,监控数据种类和量的收集大得惊人。系统从2014年切换到微服务框架后,一致运行良好,当然,我觉得这一切的功劳归功于我们刚开始坚持的“简单化”与“标准化”原则。
在系统架构演变的初期,我们在系统架构的建设以及规范上我们下了很大的功夫,始终保持着我们的两大原则,各种标准化的代码开发和日志输出为我们后续的发展打下坚实的基础。在2016年没有成立专门的“运维组”之前,整个系统(后端)都是靠我们数个后台同事开发并维护的,你没有听错,是数个,而不是数十个。也因为这个原因,让我们避免了“监控任务被隔离”以及“监控数据量太多”的常见运维问题。在我们当时数人开发兼维护的情况下,自动化维护和监控是必不可少的手段。除了定时的关键和敏感指标监控外,我们会根据整个系统的“业务拓扑关系”做一个“关联指标聚合”的展示页,可以从整体了解系统的整体概况。关键的聚合指标包括关键服务并发量、关键服务容器线程数、关键服务API耗时分布、关键Redis服务性能分布关键数据源性能分布、关键外围系统耗时分布、关键业务错误率概况以及各种TOP10的质量监控等。下图是我们运维系统初期的监视一角(没有太华丽的UI)。
关键技术二:资源/服务调度
当单体应用被拆分后,业务场景的完成必须依赖于后端各个服务之间的调度和关联来完成的。当然,最理想的状态就是服务之间没有关联(最低复杂度),直接可以满足于业务应用的消费。这不是不可能,如果系统和业务不复杂的话,可以把服务编排的工作直接交给客户端去做。我们系统的部分功能就选择了这样一种折中的方案,这样会增加了HTTP连接性能消耗,但降低了整个系统服务之间调度的复杂度。
虽然我们可以通过各种方式去规避问题,但作为分布式系统的调度问题,服务治理是不可避免的一个关键性问题,其主要的关键技术有以下几点:
服务关键程度
从以上监控层面可以看到,我们会区分关键服务和非关键服务,这种区分并非技术问题,而是业务问题。对服务关键程度的划分可以让系统在整体层面增加一个“层次感”,让系统策略可以做得更加精准,如非关键服务降级等问题。
服务依赖关系
要做到无服务依赖确实非常困难,但却是分布式系统设计的一个重点方向,所谓“没有依赖就没有伤害”。传统的SOA是通过ESB来解决服务间依赖的问题,而微服务就是分布式系统服务依赖的最优解上限,而服务依赖的下限就是千万不好有闭环依赖,出现依赖环的设计是有问题的设计。在服务依赖管理方面,我们系统对服务关联做了一定的管理措施,例如,我们服务的开发是分工的,如果服务A调用了服务B,而服务B具体又调用了什么服务,服务A的开发小组是不得而知的,所以我们在开发阶段做系统服务依赖配置的时候(必须配置,因为我们服务之间调用强制验证,这就是我们付出一定性能开销的管理成本),各个小组都可以实时看到整个系统服务的调用依赖图,当配置服务依赖的时候系统实时检查这条服务调用链的深度,并达到一定的阀值是提出告警或禁止配置,当出现这样的问题后可以提交给上层设计团队去评估设计合理性。虽然我们整个系统都是基于微服务理念设计,但开发团队大了,什么鸟都会有,这些额外的技术管理成本有时候也是无可奈何。
服务发现
有了以上的的“地图”第一步,接下来需要对这整个系统地图的服务状态和生命周期管理的重要工具,毕竟系统是由各种服务组成的,分布式之所以灵活是因为其服务的动态性,在系统运行过程中,有的服务会增加,有的服务会离开,有的服务生效中,有的服务失效了等等。从管理角度,我们需要知道一个服务注册中心来做以下几个事:
在我们的生产系统中,服务的种类、数量数量以及状态信息是具备的,但我们并没有对服务进行版本管理,只对服务的API进行版本管理,并对服务API兼容性做了强制性的“规范”要求(只能增加输出,不能修改或删除输出)。
架构版本管理
因为系统被拆分成各种服务单元,而不同的服务单元有一定概率是不同团队开发,所以会存在服务单元版本依赖问题,超大型系统甚至会存在操作系统层面以及中间件层面不同版本的管理问题。如果使用过maven项目模型管理工具的话,就会知道pom.xml文件,它管理着各个模块以及版本之间的关系,项目的建立也是基于pom.xml之上,我们的服务同样可以通过这种模式进行管理,但成本就会增加。目前在我们的生产系统中,并没有这种版本管理,我们是通过规范性手段避免了这种版本管理的问题,目前来说,暂时没有出现这种服务版本依赖性而导致回退冲突等问题。
服务生命周期管理
当我们有了服务发现中心这个管控工具之后,就需要对服务的状态进行管理了,严格来说,服务的状态会有一下几种:
① Provison:代表在供应一个新的服务;
② Ready:表示启动成功了;
③ Run:表示通过了服务健康检查;
④ Update:表示在升级中;
⑤ Rollback:表示在回滚中;
⑥ Scale:表示正在伸缩中(其中包括Scale-in和Scale-out)
⑦ Destroy:表示在销毁中
⑧ Failed:表示失败状态
如果需要控制好整个分布式系统架构,就需要对以上服务状态进行严格管理,特别是在多协作团队中,这写状态的把控十分重要,因为服务会出现各种不预期(故障)和预期(上线或更新)的变化。现在整个完整的地图、服务之间的依赖关系以及服务的状态管控,整个完整分布式系统的管理手段会让整个系统清晰起来。在我们生产系统中,对于服务的状态并没有这么严格,目前只用到“运行”、“未知”以及“异常”三种状态,部分其它状态,我们统一用“未知状态”代替了。
关键技术三:状态/数据调度
对于单体应用来说,并没有太多的“可能性”,复杂度和维护成本自然而然不会太高,所以不会存在系统拆分后的种种新问题。因为服务拆分了,并且业务场景需要各种服务之间通过调度协助来完成的,所以各个“点”之间的状态就格外重要了。这里的状态(state)指的是服务自身的数据状态,而不是运行状态(status)。一般来说,我们都会通过第三方服务维护系统服务的数据状态,如Mysql、Redis、ZooKeep或NFS等文件系统,从而让服务变成“无状态服务”,以便于服务的灵活变更和扩展,甚至可以往serverless模式发展。所以很多时候,分布式系统架构中出现的问题往往都是这些存储状态的服务,并且数据存储节点的Scale问题也落在了这些存储状态服务当中。
要解决像数据存储点这样的单点问题,就要解决数据的复制(Replication)问题,也就是让数据服务可以像无状态的服务一样在不同的设备上进行调度,而数据的复制问题又会带来数据一致性的2问题,从而带来更多的性能问题。对于解决数据副本间的一致性问题,目前比较常见的方案是有:Master-Slave方案、Master-Master方案、两阶段和三阶段提交方案、Paxos方案,而各种方案的优缺点可见下图:
在我们生产系统中,核心的业务我们主要还是用到了商业的Master-Master方案(Oracle共享存储),在非关系数据库方面(Redis)我们几乎都是自主研发的2PC方案组件。我们使用Redis的时候,最新版的稳定版本好像只有2.8,并不存在目前3.0+的集群方案,所以我们自己在应用服务层写了许多不同场景的2PC方案,为什么会很多,因为不同场景的业务不一致导致业务补偿的方案有很大的不同,许多的场景目前还一直稳定运行着。当然,从分工来看,状态数据调度应该由分布式存储层(Laas)来解决,这样对于应用层来说是透明的,就像使用单点存储一样简单。
关键技术四:流量调度
无论对单体应用还是分布式应用,流量都是需要控制的,对于单体应用而言,更多只是简单外部流量控制措施,但当系统分解后,因为服务调度的存在,所以流程同样需要调度。
分布式系统可以把流量调度分为内部调度和外部调度,内部是因为服务编排(调用链)的存在,外部是因为业务被分解隔离的缘故。对于一个流量调度器来说,其主要功能有两点:
① 可以根据系统的运行情况自动地去调整流量调度,自动化地完成整个系统的稳定性;
② 可以弹性地计算并解决系统各种突发性事件,确保系统始终保持平稳运行;
说白了,以上功能都是为了确保系统的高可用和稳定性。其实,流量组件可以完成的事情可以包含如下:
服务流量:服务发现、服务路由、服务降级、服务熔断、服务保护等;
流量控制:负载均衡、流量分配、流量控制、异地灾备(多活)等;
流量管理:协议转换、请求校验、数据缓存、数据计算等;
如果从这些功能角度去看,这个网关(API Gateway)其实就跟运维系统一样,是一个实实在在的流量调度系统。从功能种类来说,这个API网关会略显臃肿,很容易会成为“瓶颈”。所以,作为一个能扛得住的流量的API网关,“高性能实现”、“抗大流量”、“简单的业务逻辑”以及还有“服务化设计”等几大关键技术点必须得认真思考和衡量。
我们生产系统中,流量调度我们分开了三个层面来管理,总API网关入口,各服务网关入口,以及服务入口,因为我们当初考虑到网关作为入口的重要性,我们把以上API网关的部分功能(如缓存、熔断)下放到了服务实现层面去实现了。由于业务原因,我们系统并没有做得很“自动化”,全部靠监控告警以及人工熔断操作。因为客户组织复杂的组织架构原因,我们当初各种自动化容灾以及降级方案都“政治性地”否定了,只能把方案实现预留Admin API,随时通过总控制台人工干预。
最后的总结
越学习越发现,有时候努力很重要,但方法更重要。大量的零碎知识等于没有知识,知识焦虑是必然结果。知识之间是相关联的,有时候一篇数千字的技术文章,我会看上个把月。因为自身基础不扎实的原因,缺乏一定的知识高度去消化,所以许多知识点我都必须深入学习才能略懂一二。今天数千字的学习总结,背后承载的学习资料可能数十万文字的学习和研究,还有大量地反思和实践总结。有时候明明知道,但又无动于衷,这显然就是自我突破的关键点所在。