读书笔记--凤凰架构--其三

事件驱动架构
为了能让子系统互相通信,事件驱动架构的方案是在子系统之间建立一套事件队列管道,来自系统外部的消息将以事件的形式发送至管道中,各个子系统从管道里获取能够处理的事件消息,可以自己发布一些新的事件到管道队列中去,如此,每一个消息的处理者都是独立的,高度解耦的,但又能与其他处理者通过事件管道进行互动。

 

演化至事件驱动架构时远程服务调用迎来了 SOAP 协议的诞生

微服务时代
微服务是一种通过多个小型服务组合来构建单个应用的架构风格,这些服务围绕业务能力而非特定的技术标准来构建。各个服务可以采用不同的编程语言,不同的数据存储技术,运行在不同的进程之中。服务采取轻量级的通信机制和自动化的部署机制实现通信与运维。

“微服务”这个技术名词是由 Peter Rodgers 博士在 2005 年度的云计算博览会提出的。“Micro-Web-Service”,指的是一种专注于单一职责的、语言无关的、细粒度 Web 服务。最初的微服务可以说是 SOA 发展时催生的产物。随着时间的推进,技术的发展,微服务已经不再是维基百科定义的那样,“仅仅只是一种 SOA 的变种形式”了。

微服务真正的崛起是在 2014 年,Martin Fowler 与 James Lewis 合写的文章《Microservices: A Definition of This New Architectural Term》中 给出了现代微服务的概念: “微服务是一种通过多个小型服务组合来构建单个应用的架构风格,这些服务围绕业务能力而非特定的技术标准来构建。各个服务可以采用不同的编程语言,不同的数据存储技术,运行在不同的进程之中。服务采取轻量级的通信机制和自动化的部署机制实现通信与运维。”

文中列举了微服务的九个核心的业务与技术特征:

围绕业务能力构建
分散治理
通过服务来实现独立自治的组件
产品化思维
数据去中心化
强终端弱管道
容错性设计
演进式设计
基础设施自动化
《Microservices》文中除了定义微服务是什么,还专门申明了微服务不是什么——微服务不是 SOA 的变体或衍生品,应该明确地与 SOA 划清了界线,不再贴上任何 SOA 的标签。

微服务追求的是更加自由的架构风格,摒弃了几乎所有 SOA 里可以抛弃的约束和规定,提倡以“实践标准”代替“规范标准”。没有了统一的规范和约束,服务的注册发现、跟踪治理、负载均衡、故障隔离、认证授权、伸缩扩展、传输通信、事务处理,等等这些问题,在微服务中不再会有统一的解决方案,即使只讨论 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),等等。其他领域的情况也是与此类似,总之,完全是八仙过海,各显神通的局面。

作为一个普通的服务开发者,“螺丝钉”式的程序员,微服务架构是友善的。可是,微服务对架构者是满满的恶意,因为对架构能力要求已提升到史无前例的程度

后微服务时代
定义:从软件层面独力应对微服务架构问题,发展到软、硬一体,合力应对架构问题的时代,此即为“后微服务时代”。

当虚拟化的基础设施从单个服务的容器扩展至由多个容器构成的服务集群、通信网络和存储设施时,软件与硬件的界限便已经模糊。一旦虚拟化的硬件能够跟上软件的灵活性,那些与业务无关的技术性问题便有可能从软件层面剥离,悄无声息地解决于硬件基础设施之内,让软件得以只专注业务,真正“围绕业务能力构建”团队与产品。

Kubernetes 成为容器战争胜利者标志着后微服务时代的开端,但 Kubernetes 仍然没有能够完美解决全部的分布式问题——“不完美”的意思是,仅从功能上看,单纯的 Kubernetes 反而不如之前的 Spring Cloud 方案。这是因为有一些问题处于应用系统与基础设施的边缘,使得完全在基础设施层面中确实很难精细化地处理。

举个例子,微服务 A 调用了微服务 B 的两个服务,称为 B1和 B2,假设 B1表现正常但 B2出现了持续的 500 错,那在达到一定阈值之后就应该对 B2进行熔断,以避免产生雪崩效应。如果仅在基础设施层面来处理,这会遇到一个两难问题,切断 A 到 B 的网络通路则会影响到 B1的正常调用,不切断的话则持续受 B2的错误影响。

 

以上问题在通过 Spring Cloud 这类应用代码实现的微服务是可以处理和解决的,只受限于开发人员的想象力与技术能力,但基础设施是针对整个容器来管理的,粒度相对粗旷,只能到容器层面,对单个远程服务就很难有效管控。类似的情况不仅仅在断路器上出现,服务的监控、认证、授权、安全、负载均衡等都有可能面临细化管理的需求,譬如服务调用时的负载均衡,往往需要根据流量特征,调整负载均衡的层次、算法,等等,而 DNS 尽管能实现一定程度的负载均衡,但通常并不能满足这些额外的需求。

为了解决这一类问题,虚拟化的基础设施很快完成了第二次进化,引入了“服务网格”(Service Mesh)的“边车代理模式”(Sidecar Proxy),所谓的“边车”是由系统自动在服务容器(通常是指 Kubernetes 的 Pod)中注入一个通信代理服务器,以类似网络安全里中间人攻击的方式进行流量劫持,在应用毫无感知的情况下,悄然接管应用所有对外通信。这个代理除了实现正常的服务间通信外(称为数据平面通信),还接收来自控制器的指令(称为控制平面通信),根据控制平面中的配置,对数据平面通信的内容进行分析处理,以实现熔断、认证、度量、监控、负载均衡等各种附加功能。这样便实现了既不需要在应用层面加入额外的处理代码,也提供了几乎不亚于程序代码的精细管理能力。

很难从概念上判定清楚一个与应用系统运行于同一资源容器之内的代理服务到底应该算软件还是算基础设施,但它对应用是透明的,不需要改动任何软件代码就可以实现服务治理,这便足够了。服务网格在 2018 年才火起来,今天它仍然是个新潮的概念,仍然未完全成熟,甚至连 Kubernetes 也还算是个新生事物。但作者提出,未来 Kubernetes 将会成为服务器端标准的运行环境,如同现在 Linux 系统;服务网格将会成为微服务之间通信交互的主流模式,把“选择什么通信协议”、“怎样调度流量”、“如何认证授权”之类的技术问题隔离于程序代码之外,取代今天 Spring Cloud 全家桶中大部分组件的功能,微服务只需要考虑业务本身的逻辑,这才是最理想的解决方案。

无服务时代
如果说微服务架构是分布式系统这条路的极致,那无服务架构,也许就是“不分布式”的云端系统这条路的起点。

虽然发展到了微服务架构解决了单台机器的性能无法满足系统的运行需要的问题,但是获得更好性能的需求在架构设计中依然占很大的比重。对软件研发而言,不去做分布式无疑才是最简单的,如果单台服务器的性能可以是无限的,那架构演进一定不是像今天这个样子。

绝对意义上的无限性能必然是不存在的,但在云计算落地已有十年时间的今日,相对意义的无限性能已经成为了现实。2012 年,Iron.io 公司率先提出了“无服务”的概念,2014 年开始,亚马逊发布了名为 Lambda 的商业化无服务应用,并在后续的几年里逐步得到开发者认可,发展成目前世界上最大的无服务的运行平台;到了 2018 年,中国的阿里云、腾讯云等厂商也开始跟进,发布了旗下的无服务的产品,“无服务”已成了近期技术圈里的“新网红”之一。

无服务现在还没有一个特别权威的“官方”定义,但它的概念并没有前面各种架构那么复杂,本来无服务也是以“简单”为主要卖点的,它只涉及两块内容:后端设施和函数。

后端设施是指数据库、消息队列、日志、存储,等等这一类用于支撑业务逻辑运行,但本身无业务含义的技术组件,这些后端设施都运行在云中,无服务中称其为“后端即服务”(Backend as a Service,BaaS)。
函数是指业务逻辑代码,这里函数的概念与粒度,都已经很接近于程序编码角度的函数了,其区别是无服务中的函数运行在云端,不必考虑算力问题,不必考虑容量规划,无服务中称其为“函数即服务”(Function as a Service,FaaS)。
无服务的愿景是让开发者只需要纯粹地关注业务,不需要考虑技术组件,后端的技术组件是现成的,可以直接取用,没有采购、版权和选型的烦恼;不需要考虑如何部署,部署过程完全是托管到云端的,工作由云端自动完成;不需要考虑算力,有整个数据中心支撑,算力可以认为是无限的;也不需要操心运维,维护系统持续平稳运行是云计算服务商的责任而不再是开发者的责任。

作者认为无服务很难成为一种普适性的架构模式,因为无服务不适配于所有的应用。对于那些信息管理系统、网络游戏等应用,所有具有业务逻辑复杂,依赖服务端状态,响应速度要求较高,需要长链接等这些特征的应用,至少目前是相对并不适合的。因为无服务天生“无限算力”的假设决定了它必须要按使用量计费以控制消耗算力的规模,所以函数不会一直以活动状态常驻服务器,请求到了才会开始运行,这导致了函数不便依赖服务端状态,也导致了函数会有冷启动时间,响应的性能不可能太好

作者认为软件开发的未来不会只存在某一种“最先进的”架构风格,多种具针对性的架构风格同时并存,是软件产业更有生命力的形态。笔者同样相信软件开发的未来,多种架构风格将会融合互补,“分布式”与“不分布式”的边界将逐渐模糊,两条路线在云端的数据中心中交汇。

架构师的视角
访问远程服务
远程服务调用
进程间通信
RPC 出现的最初目的,就是为了让计算机能够跟调用本地方法一样去调用远程方法。

进程间通信的方式有

管道,又叫具名管道
管道类似于两个进程间的桥梁,可通过管道在进程间传递少量的字符流或字节流。普通管道只用于有亲缘关系进程(由一个进程启动的另外一个进程)间的通信,具名管道摆脱了普通管道没有名字的限制,除具有管道所有的功能外,它还允许无亲缘关系进程间的通信。管道典型的应用就是命令行中的|操作符,比如:

ps -ef | grep java

ps与grep都有独立的进程,以上命令就通过管道操作符|将ps命令的标准输出连接到grep命令的标准输入上。

信号
信号用于通知目标进程有某种事件发生,除了用于进程间通信外,进程还可以发送信号给进程自身。信号的典型应用是kill命令,比如:

kill -9 pid

以上就是由 Shell 进程向指定 PID 的进程发送 SIGKILL 信号。

信号量
信号量用于两个进程之间同步协作手段,它相当于操作系统提供的一个特殊变量,程序可以在上面进行wait()和notify()操作。

消息队列
以上三种方式只适合传递传递少量信息,消息队列用于进程间数据量较多的通信。进程可以向队列添加消息,被赋予读权限的进程则可以从队列消费消息。消息队列克服了信号承载信息量少,管道只能用于无格式字节流以及缓冲区大小受限等缺点,但实时性相对受限。

共享内存
允许多个进程访问同一块公共的内存空间,这是效率最高的进程间通信形式。原本每个进程的内存地址空间都是相互隔离的,但操作系统提供了让进程主动创建、映射、分离、控制某一块内存的程序接口。当一块内存被多进程共享时,各个进程往往会与其它通信机制,譬如信号量结合使用,来达到进程间同步及互斥的协调操作。

套接字接口
以上两种方式只适合单机多进程间的通信,套接字接口是更为普适的进程间通信机制,可用于不同机器之间的进程通信。套接字(Socket)起初是由 UNIX 系统的 BSD 分支开发出来的,现在已经移植到所有主流的操作系统上。出于效率考虑,当仅限于本机进程间通信时,套接字接口是被优化过的,不会经过网络协议栈,不需要打包拆包、计算校验和、维护序号和应答等操作,只是简单地将应用层数据从一个进程拷贝到另一个进程,这种进程间通信方式有个专名的名称:UNIX Domain Socket,又叫做 IPC Socket

通信的成本
因为Socket是网络栈的统一接口,所以基于套接字接口的通信方式不仅适用于本地相同机器的不同进程间通信,也能支持基于网络的跨机器的进程间通信。比如 Linux 系统的图形化界面中,X Window 服务器和 GUI 程序之间的交互就是由这套机制来实现。由于 Socket 是各个操作系统都有提供的标准接口,所以可以把远程方法调用的通信细节隐藏在操作系统底层,从应用层面上看来可以做到远程调用与本地的进程间通信在编码上完全一致。这种透明的调用形式却造成了程序员误以为通信是无成本的假象,因而被滥用以致于显著降低了分布式系统的性能。1987 年,在“透明的 RPC 调用”一度成为主流范式的时候,Andrew Tanenbaum 教授曾发表了论文对这种透明的 RPC 范式提出了一系列质问,论文的中心观点是,本地调用与远程调用当做一样处理,这是犯了方向性的错误,把系统间的调用做成透明,反而会增加程序员工作的复杂度。此后几年,关于 RPC 应该如何发展、如何实现的论文层出不穷。最终,到 1994 年至 1997 年间,一众大佬们共同总结了通过网络进行分布式运算的八宗罪

The network is reliable —— 网络是可靠的。
Latency is zero —— 延迟是不存在的。
Bandwidth is infinite —— 带宽是无限的。
The network is secure —— 网络是安全的。
Topology doesn't change —— 拓扑结构是一成不变的。
There is one administrator —— 总会有一个管理员。
Transport cost is zero —— 不必考虑传输成本。
The network is homogeneous —— 网络是同质化的。
以上这八条反话被认为是程序员在网络编程中经常被忽略的八大问题,潜台词就是如果远程服务调用要弄透明化的话,就必须为这些罪过埋单,这算是给 RPC 是否能等同于 IPC 来实现暂时定下了一个具有公信力的结论。至此,RPC 应该是一种高层次的或者说语言层次的特征,而不是像 IPC 那样,是低层次的或者说系统层次的特征成为工业界、学术界的主流观点。

远程服务调用的定义:远程服务调用是指位于互不重合的内存地址空间中的两个程序,在语言层面上,以同步的方式使用带宽有限的信道来传输程序控制信息。

 

三个基本问题
从20 世纪 80 年代中后期开始直至接下来几十年来所有流行过的 RPC 协议,都不外乎变着花样使用各种手段来解决以下三个基本问题:

如何表示数据
这里数据包括了传递给方法的参数,以及方法执行后的返回值。

如何传递数据
如何通过网络,在两个服务的 Endpoint 之间相互操作、交换数据。

如何确定方法
“如何表示同一个方法”,“如何找到对应的方法”还是得弄个跨语言的统一的标准才行。

以上 RPC 中的三个基本问题,全部都可以在本地方法调用过程中找到相对应的操作。RPC 的想法始于本地方法调用,尽管早已不再追求实现成与本地方法调用完全一致,但其设计思路仍然带有本地方法调用的深刻烙印。

统一的RPC
1、面向透明的、简单的 RPC 协议,如 DCE/RPC、DCOM、Java RMI,要么依赖于操作系统,要么依赖于特定语言,总有一些先天约束;

2、面向通用的、普适的 RPC 协议;如 CORBA,就无法逃过使用复杂性的困扰,CORBA 烦琐的 OMG IDL、ORB 都是很好的佐证;

3、通过技术手段来屏蔽复杂性的 RPC 协议,如 Web Service,又不免受到性能问题的束缚。

对于RPC协议,简单、普适、高性能这三点,似乎真的难以同时满足。

 

posted @ 2021-10-30 19:15  巩云龙  阅读(53)  评论(0编辑  收藏  举报