编辑推荐: |
本文主要介绍了软件架构组成与架构风格、常见基于网络应用的架构风格、面向服务(SOA)的架构风格推导、SOA
的架构元素及SOA相关其它问题讨论 。
本文来自于微信公众号焉知智能汽车,由火龙果软件linda编辑、推荐。
|
|
本文为系列文章:
自动驾驶软件架构之:中间件与SOA(一)
自动驾驶软件架构之:中间件与SOA(二)
自动驾驶软件架构之:中间件与SOA(三)
3 软件架构方法论及 SOA推导
前面讲了很多中间件产品中常用的关键技术。相对更大层面的软件架构来说,这些只是局部的技术点。用于某个行业领域的中间件产品往往会非常深度地决定这个行业领域应用软件所采用的软件架构。
软件系统规模比较小的时候,我们很少用架构这个词。所以早期有种说法:
算法+数据结构=程序
这里的程序指的是解决特定的具体问题,一般不涉及大范围网络数据交换的独立软件。互联网让软件应用从小范围专业领域变成覆盖全球的信息系统,软件系统也从简单的程序演变出很多复杂的架构。
目前在汽车软件领域也正经历着类似的变化,由于智能网联与自动驾驶的需求,汽车软件的复杂度也以指数形式上升,同时由于以太网的引入,很多之前在互联网上可以使用的软件架构经过一些变换后也可以用到汽车软件上。典型的就是现在大家常说的SOA。
SOA是什么?更专业的说法,SOA是一种软件架构风格。车载中间件产品也会有其软件架构及架构风格,SOA
目前看来会是一个主流的趋势。
什么是软件架构,什么是架构风格,需要一个清晰的定义,这一章先从这里开始。
3.1 软件架构组成与架构风格
在这里,我先引用两篇论文,
1、软件架构研究基础(Foundations for the Study of Software
Architecture [24])
2、架构风格与基于网络的软件架构设计Architectural Styles and the Design
of Network-based SoftwareArchitectures [23]
第一篇是1992年的论文,提出了软件架构的基础模型和架构风格的概念。第二篇的作者Roy Thomas
Fielding 是 HTTP1.0/1.1 规范的主要制定者,这篇文章是他2000年的博士论文,在Web发展史上,这是一篇极其重要的经典文献,奠定了现代
Web 架构的基础。这都是20-30年前的文章,但是其对软件架构的阐述丝毫没有过时,一样在理论上指导着软件架构的设计。
很多汽车相关企业都在推进SOA化,但其架构风格背后的推理逻辑其实并不是显而易见的。只看到具体的技术点,而不知其由来,就很难准确理解并使用它。尤其是现代的汽车电子电器架构就是基于多种车载网络体系来构建,汽车软件已经成了典型的基于网络的分布式软件系统。原来基于网络的软件架构设计原理对汽车软件一样有非常重要的参考作用。
这一节尽量以易于理解的方式,用较短的篇幅将这两篇文章中关于软件架构和架构风格的阐述做一个综述。为后续的讨论做一个理论基础。
3.1.1 软件架构研究方法论
图3. 1以 UML 类图的表示了组成软件架构的基本概念。
注: 简单的 UML 符号语意。
图片表示“泛化(抽象)”概念,也就是逻辑上一般化与具体的关系,程序语言的继承。箭头所指父类,即比较“抽象”的概念,另一端是该概念的具体化呈现。
图片表示“组成”关系,也就是整体与部分的关系。箭头所指为整体,另一端为组成整体的各个部分。
图片表示遵循某个“规约”,程序语言中代表接口实现。箭头所指为具体的规约规则。
![](images/2021120341.jpg)
图3. 1软件架构组成
软件架构由三个方面组成,架构元素,架构的组成形式,和一些形成架构的基本原则。架构元素有三种:
处理元素:执行实际的功能性运算与数据转换;是“计算和状态的所在地”;是在运行时执行某种功能的软件单元。
数据元素:承载着被使用和转换的信息。
连接元素:将架构的不同部分结合在一起的粘合剂。
我们用四个不同的领域概念类比来理解这些架构元素的含义。
架构的组成形式中包括“配置关系”与“约束属性”。“配置关系”是在系统的运行期间处理元素、连接元素和数据元素之间的关系结构。“约束属性”用于约束架构元素的选择。它于将架构元素约束到系统需求所需的程度。对应的实例如下表。
架构的一个潜在但不可或缺的部分是在定义架构时做出的各种选择的一些基本原则。在软件架构中,基本原则解释了如何满足系统约束。这些约束是由从基本功能方面到各种非功能方面的考虑因素决定的,例如经济性、性能、和可靠性等。
结合上面所述,我们可以把设计一个软件架构描述为:
根据我们需要构建的软件系统的约束需求,我们选择一组基本的原则,在这组原则的指导下,选择合适(约束符合)的架构元素(处理元素、数据元素连接元素)组成一个集合,并设计各种架构元素的关系结构。
不同的基本原则选择方式,会让软件架构呈现出不同的风格(Style),我们称之为架构风格。一种架构风格是一组协作的架构约束,这些约束限制了架构元素的角色和功能,以及架构元素之间的关系。
当我们谈及某种形式的软件架构时,实际上往往讨论的是架构风格,比如说 SOA。
每个架构设计决策可以被看作是对一种风格的应用,而一个软件架构往往会混用多种风格。
3.1.2 软件架构的评估方法
一种架构风格是一组协作的架构约束,但是经常会出现一种情况,一种约束的效果可能会抵消一些其它的约束所带来的好处。没有完美的设计,获得某种优势的同时,可能需要在另一方面付出代价。所以我们需要一种评估机制去从多个方面去评估一个软件架构的特性,以便我们在不同的可能性之间进行权衡。
性能
性能往往是软件架构首先要考虑的方面,软件架构需要满足应用的性能需求。
对于 I/O 性能,我们关注的一般是总吞吐量和平均的传输延迟。对于计算性能我们关注的是计算单元的总利用率以及计算任务的响应延迟。一个是衡量系统的总效率,一个是衡量系统的单次响应能力,这个会影响到用户可察觉的性能。
这两者有时是有冲突的。好的架构风格要能在满足响应性要求的情况下,尽可能支持系统能够达到的较高的总效率。
性能也受成本的约束,在移动平台或车载平台,性能还受功耗的约束。
可伸缩性
可伸缩性要求架构能支持从小规模到大规模的平滑扩展。架构需要能够支持大量的组件以及这些组件之间交互的能力。可伸缩性能够通过以下方法来改善:
简化组件
将服务分布到很多组件(分散交互)
根据监视的结果对组件之间的交互进行动态控制
风格可以通过确定应用状态的位置、分布的范围以及组件之间的耦合度,来影响这些因素。
简单性
如果分配给单独组件的功能足够简单,那么它们就更容易被理解和实现,也方便进行测试。越简单的组件也越能够被重复使用。架构要能够支持将复杂的功能分解为很多简单的组件,同时还要能够交互协同以完成预期的功能。就是要拆得开,还能合得起来。
可修改性
需求也会随时间发生变化,可修改性是对于应用的架构所作的修改的容易程度。可修改性能够被进一步分解为在下面所描述的可进化性、可扩展性、可定制性、可配置性和可重用性。
可进化性:
一个组件实现能够被改变而不会对其它组件产生负面影响的程度。
可扩展性:
将功能添加到一个系统中的能力。动态可扩展性意味着功能能够被添加到一个已部署的系统中,而不会影响到系统的其它部分。提高可扩展性的方法是在一个架构中减少组件之间的耦合,比如基于事件或消息进行交互。
可定制性:
指组件可以为一个客户进行定制化扩展,而不会对该组件的其它客户产生影响。支持可定制性的风格也可能会提高简单性和可扩展性,这是因为通过仅仅直接实现最常用的服务,允许客户端来定义不常用的服务,服务组件的尺寸和复杂性将会降低。
可配置性
指在部署后对于组件,或者对于组件配置的修改,这样组件能够使用新的服务或者新的数据元素类型。管道/过滤器风格和按需代码风格就是典型的例子。
可重用性
一个应用的架构中的处理元素、连接元素或数据元素能够在不做修改的情况下在其它应用中重用。在架构风格中提高可重用性的主要方法就是是降低组件之间的耦合(对于其它组件的标识的了解)和强制使用通用的组件接口。
可见性
指对组件之间的交互进行监视或仲裁的能力。可以通过以下方式提高可见性:交互的共享缓存、通过分层服务提供可伸缩性、通过反射式监视(reflective
monitoring)提供可靠性、以及通过允许中间组件(例如,网络防火墙)对交互做检查提供安全性。风格能够通过限制必须使用通用性的接口,或者提供访问监视功能的方法,来影响基于网络的应用中交互的可见性。比如在自动驾驶应用中,我们强制每个算法组件报告它接收数据、处理数据、发送数据的帧率。
可移植性
如果软件能够在不同的环境下运行,软件就是可移植的。标准的通讯协议,标准化的API接口都可以提高软件的可移植性。SOME/IP
协议只管通讯的数据交换格式,可以兼容不同的通讯库的实现。AdaptiveAutoSAR 标准将应用程序可以使用的系统调用限制为
POSIX PSE51 标准(参见 4.3.1.1),方便移植到不同的 OS(Linux/QNX/VxWorks)
;同时提供标准的应用API接口,支持基于Adaptive AutoSAR的应用在不同的 Adaptive
AutoSAR实现之间移植。
可靠性
从应用的架构角度来说,可靠性可以被看作当在处理元素、连接元素或数据之中出现部分故障时,一个架构容易受到系统层面故障影响的程度。架构风格能够通过以下方法提高可靠性:避免单点故障、增加冗余、允许监视、以及用可恢复的动作来缩小故障的范围。车载应用还有更高的功能安全要求。
3.2 常见基于网络应用的架构风格
图3. 2列举出了大多数基于网络的应用架构风格。有一些与网络应用相关性不大的其它架构风格没有放进来。
图3. 2常见的软件架构风格之间的关系
图中偏左侧黑色粗框标识的是几个基础风格,包括“客户-服务器,管道和过滤器、多副本,分层系统,虚拟机/解释器;基于事件的集成”等。其它风格是对这些基础风格的继承(或称为扩展),有的风格是继承自多个基础风格。
每个风格上部以淡粉色标注的标签表示正面的评估结果,如改进“网络性能、可伸缩性、可靠性等”,下部以淡青色标注的标签表示负面的评估结果。这里我们不会逐一介绍每个风格,而是在下文对SOA
风格的推导中叙述涉及到的风格。
3.3 面向服务(SOA)的架构风格推导
汽车软件最近开始了向 SOA 转型的趋势。从软件架构角度看,SOA是一组软件架构风格的统称。严格来说,SOA并不是一个单一的软件架构风格,而是一系列各具特点的软件架构风格的综合运用,其中每一种架构风格都推崇架构元素之间的一种特定的交互类型。
我们从一个空的架构风格开始,逐步增加新的约束,从而推导出 SOA 的架构风格,并结合车载软件和自动驾驶软件的特点来做进一步的说明。
3.3.1 “客户-服务器”风格
首先我们加入到我们约束集合中的是“客户-服务器”风格。客户-服务器约束背后的原则是关注点分离。“关注点分离”是软件设计思想中的一个关键概念,几乎可以用在软件设计从架构到具体实现的各个方面。这个概念比较抽象,简单理解,可以认为“一个软件单元(架构组件,软件模块,接口等),其关注的范围尽可能小,聚焦在某一个特定的领域范围(关注点)。”一个关注点可以看作是“功能,行为,数据”等,很难有一个通用准确的概括。不同的关注点由不同的软件单元来处理,软件的耦合程度就会降低,会带来架构和实现上的各种便利。
“客户-服务器”风格首先分离了“功能实现”与“用户接口”两个关注点。“功能实现”一般包括对数据的处理、计算、存储,“用户接口”是用户提供数据和获取结果的界面。这两者的分离可以让“功能实现”部分单独进化,而不影响用户的使用。在用户接口不变的情况下,“功能实现”可以采用更新的算法,更快的存储,更大的部署规模,或者移植到不同的技术平台,而这些对用户都是透明的。
对复杂的软件系统而言,把整个系统拆解成多个服务器程序,每个服务器程序关注特定的功能。这种拆分本身也是关注点分离思想的应用。
现代车载软件以及自动驾驶系统极为复杂,需要很多家不同供应商开发不同的软件组件。客户-服务器风格以服务界定功能边界,不同供应商开发按照预定义的接口实现特定的服务,为其它组件提供服务的同时也使用其它供应商开发的服务组件,只要接口定义好,多个不同的供应商的软件组件就可以协同工作。
图3. 4 SOA:客户-服务器
3.3.2 状态分离与局部化
3.3.2.1程序“状态”的含义
“状态”这个词被用在很多地方,其含义往往有很多细微差别,容易被混淆。这里所谓的状态是指“某个软件组件内部包含的数据信息,这个数据信息会影响外部对这个软件组件发出请求的响应结果”。
假设有一个加法器A,提供了两个接口:
1、设置初始值
2、增加 n 并返回结果
我们给加法器A设置初始值0,然后每次加1 ,返回的结果是不一样的。
对另一加法器 B,提供如下接口:
输入两个操作数,返回其加法结果
对加法器B而言,只要每次给出相同的输入数据,返回的结果是一样的,不依赖于加法器B的内部数据。
加法器A内部就保存了程序状态,假如多个客户端并发进行访问,取得的结果就会互相干扰。加法器B就是我们常说的无状态服务器,所有状态数据保存在客户端的请求中,多个客户端并发调用互不影响。这样就允许我们复制部署多个加法器B,分担承接大量并发请求。
图3. 5描述了多种软件架构风格在“状态分布”和“交互耦合程度”上的分布情况。这里我们先关注状态分布。图中纵轴的上部表示状态偏向在服务端保存,下端表示状态偏向在客户端保存。
图3. 5不同架构风格的状态与交互耦合程度的分布
状态偏向服务端的极端案例是“远程会话”风格,每个客户端在服务器上启动一个会话,然后调用服务器的一系列服务接口,最后退出会话。应用状态被完全保存在服务器上。如:FTP服务,Telnet服务等。
状态偏向客户端的极端案例是“客户-无状态-服务器”风格,从客户端发到服务器的每个请求必须包含用于理解请求所必需的全部信息,不能利用任何保存在服务器上的上下文(context),会话状态全部保存在客户端。
其它设计风格的“状态分布”模式处于两个极端中间。加入缓存机制的设计风格部分状态保存在客户与服务器之间的缓存机制上。分布式对象为基础的设计风格,状态主要保存在远程对象中,偏向服务器。“管道和过滤器”风格和“基于事件集成”风格没有明显的客户和服务器端,状态保存在各自组件中。
3.3.2.2 SOA服务的状态分布选择
前面说的是程序状态分布的通用概念。现在回到车载软件SOA风格。我们新增一条约束,“分离强状态服务与无状态服务,并控制状态在局部范围”。
这个约束的核心含义有两点:
1、一个是无状态服务与强状态服务要分离在不同的服务中
2、每一个服务要么是无状态,要么是强状态,避免中间路线。
无状态服务会显著改善服务的可见性、可靠性和可伸缩性。改善了可见性是因为监视系统仅仅只需要对单个请求进行分析就能得到其全部特质,不需要关心其它请求。改善了可靠性是因为它让从局部故障中恢复所需要做的工作减少了。改善了可伸缩性是因为服务器不必在多个请求之间保存状态,请求结束就可以迅速释放资源。服务器的实现得到简化,负载均衡也容易实现[23]5.1.3。
车载软件对于可见性和可靠性的要求是显而易见的。而对于可伸缩性的要求不高,因为车载软件高并发的场景并不多。但是只需要关注单个请求的实现,并迅速释放资源,依然会让服务器的实现简化很多,同时也会促进可靠性的提高。
对自动驾驶软件来说,单个请求的独立性,也意味着附着在请求上的功能性和非功能性约束也更为清晰明确。功能性约束体现在请求的参数和响应结果的数据形式上,因为一次服务只需要一次请求响应,约定好请求响应的数据规范就能界定服务的功能边界。非功能性约束往往体现在响应时间(或帧率),数据传递的
QoS 要求上,服务越简单,这些非功能性约束也就越容易明确。一方面可见性的提高能对这些非功能性约束做更好的监控,另一方面服务实现上满足这些非功能性约束也会越容易。比如,对响应时间的约束满足体现在任务调度机制上,无状态带来的简单话意味着实现良好任务调度机制就容易许多。
虽然无状态带来了诸多好处,但是在应用中状态依然是存在的。某些自动驾驶功能其状态往往需要用复杂的“有限状态机(FSM)”来定义。那如何来设计这些对状态强依赖的服务。解决的办法是:
1、在服务划分上分离无状态服务与有状态服务
2、将状态限制在服务的局部范围,即少量特定的SOA服务
在服务划分上,我们应该尽量把能够进行的无状态化处理的服务识别出来,并按照无状态的方式去定义其服务接口并实现。而把涉及到复杂状态转换的部分集中在一个独立的服务中。不同的有状态服务之间,其各自涉及的状态范围应该是正交的,即不同服务的状态相互无关。各自服务将状态限制在自己服务本地,甚至还可以对外呈现出一定的无状态特征。
例如,对于一个 ACC 应用,涉及的服务可以简化的分解为如图3. 6所示的多个服务(只是简化表示)。我们可以把ACC状态机集中在一个ACC
会话服务中。它所依赖的其它服务是无状态的,只是根据输入产生对应的输出。
实际情况会更复杂一些,比如“前视算法服务”中并不是完全无状态,如果需要做多帧融合或者目标跟踪,其结果跟多帧的数据相关。这多帧的历史数据就是状态。解决的办法是进一步拆解成更小的粒度。目标跟踪算法做成单独的服务,输入是所有关联帧的数据。
SOA服务划分的无状态和有状态的分离,在形式上与函数式编程范式中的纯函数与副作用的分离相对应,只是描述的是不同粒度上的架构问题。所以也可以在前视算法服务内部再做更细粒度的状态分离。
也就是说,向“前视算法服务”这样的轻量级状态可以通过内部或外部的进一步分解来做到真正的无状态。服务划分得过于零碎,会导致服务部署配置的难度和额外的通讯开销,但这可以通过其它的技术优化手段来解决,后文会详述。
图3. 6参与 ACC 功能的服务(简化图)
图中的“ACC会话服务”的状态就复杂的多。当用户启动ACC 功能,从功能激活到退出是一个完整的会话过程,会话的状态细节由状态机进行控制。这是典型的“远程会话”架构风格,这与一个Telnet
会话其实是非常相似的,都有一个会话生命周期过程。只不过在一辆车上,一个 ACC 会话同一时间只会出现一次,单辆车上不会出现同时多个ACC
会话实例。这个会话服务未必就没有办法是无法拆解成无状态的形式,但是会导致大量的状态数据在每次请求中传输,同时实现上没有状态机形式更自然,徒增复杂性。
设计某一个具体的车载SOA服务时,对服务状态分布的选择最好在强状态的“远程会话”和“无状态”两个极端风格中二选一。应该避免在客户端和服务端都维护状态数据。
从“关注点分离”的视角看,“无状态”化设计分离了状态“数据的存储与传输”和“状态数据的处理”两个关注点。
图3. 7 SOA:客户-服务器-状态分布
3.3.3 服务发现
复杂的软件系统被分解为大量小规模的服务后,服务之间也会有很多依赖关系,某个服务同时也会作为客户端访问其它服务。一个客户端访问另一个服务,需要知道该服务的访问点,对于TCP/IP
协议栈来说,至少包含IP和Port信息。同时,还需要知道该服务是否可用。因为服务可能还未启动,或者在启动中,或者因为某种原因停止了服务。
服务的访问点是可以通过配置文件静态配置的,如果系统中只有几个服务静态配置难度还不大,如果服务数量上升到几十个甚至更多,静态配置的维护难度就非常大。
某个服务启动时,为了它所依赖的服务已经就绪,就需要对服务的启动顺序进行管理。这对大量服务并存的系统也是很难做到的。
因此,我们给 SOA 架构风格增加一条约束“每个服务具备能被其它服务发现的能力,也能查找需要使用的其它服务”。
所谓实现被其它服务发现的能力,意味着该服务应该至少具备一下两个能力:
1、服务可用性状态发生变化时能通知其它服务
2、响应对服务可用性的查询
第1条是事件发生时的主动通知,不关心谁接收。第2条是主动响应对本服务可用状态的查询。这也意味着每个服务需要维护自己的可用性状态。
图3. 8 SOA:客户-服务器-状态分布-服务发现
除了事件性的通知机制,“服务发现”也需要包括主动查询服务可用性的能力。上图显示了在 SOA架构风格上增加“服务发现”后的图示和约束。
相对于静态配置,“服务发现”实际上提供了动态配置的能力,提高了系统的可维护性和可配置性。因为服务不是静态配置的,当一个服务失效时,可以很快的用另一个相同功能的服务替换掉它,新服务的访问点信息会很快在系统中被其它服务获取,系统可以很快能从服务失效引起的错误中恢复,提高了系统的可靠性。当某个服务需要被升级时,也可以采用类似的方式进行,对系统的可进化性也有显著帮助。
3.3.4 基于“事件/消息”发布订阅
我们再增加一条约束,“服务之间支持基于‘事件/消息’的发布订阅机制,以降低服务之间的耦合性。”
关于这个约束有很多称呼方式,含义接近但又各有侧重点,如:事件总线(EventBus), 消息通讯,发布订阅模式等。
“事件总线”的称呼,关注点在于系统中事件的触发,比如UI程序中的用户交互,或者OS内核的中断。事件发生后“广播”出来,由感兴趣的软件模块去处理。事件产生源不关心事件的处理者是谁。但是对于本地程序来说事件的触发到事件的处理可能是在一个线程里同步执行的。
“消息通讯”关注点在于数据的传输方式以及隐含的消息的异步处理语意。意味着发送者发出“消息”后,就不再拥有消息数据的内存所有权(“泼出去的水”)。发送者和消息接收者对消息数据的处理是异步的,发送者不用等待接收者确认。
“事件总线”和“消息通讯”都可以实现“发布/订阅”模式。这里发布者和订阅者之间只共享“事件名称”,或称作“消息主题”。发布者按主题发布消息,不关心谁会收到;订阅者按主题接收消息,不关心消息从哪里来。
这种特性让软件模块的测试也变得非常方便,我们可以在非生产环境中发送模拟的消息来测试软件的功能。可以录制生产环境的消息然后线下回放来做仿真测试。
图3. 9 SOA:客户-服务器-服务发现-发布订阅
“发布/订阅”风格显著降低了系统各组件之间的耦合度。添加订阅某个主题消息件的新组件变得非常容易(可扩展性)、只要组件接收或发送消息格式(接口)确定,该组件就可以被用在任何支持这个消息格式的场合(可重用性);允许组件被替换而不会影响其它组件的接口(可进化性)。发布订阅风格为可扩展性、可重用性和可进化性提供了强有力的支持。
发布/订阅的一个缺点是:难以预料一个事件将会产生什么样的响应(缺乏可理解性),事件通知并不适合交换大粒度的数据,而且也不支持从局部故障中恢复。
上图为我们的 SOA 架构增加了基于“事件/消息”发布订阅风格。多个服务之间有相互交互的方式,交互方式有基于RPC的“请求/响应”,也有基于“事件/消息”的发布订阅方式。
从“关注点分离”的视角看,发布订阅分离了数据的“生产者”和“消费者”两个关注点。
3.3.5 服务代理
车载软件发展了几十年,有大量的稳定成熟的既有代码。车内广泛使用的网络总线也有Can、 Lin、 FlexRay
等很多种,连接在这些网络上的ECU 很难去支持服务发现、发布订阅等机制。对于这些成熟的既有系统,可以为它们增加一个代理服务。代理服务仍然按照原有的方式(如:Can
总线)跟既有系统进行通讯。但是代理服务对外可以以独立SOA服务的方式呈现,提供标准的访问接口, 接口暴露既有系统可以开放的部分能力。下图在SOA架构风格上增加了服务代理风格。
图3. 10客户-服务器-状态分布-服务发现-发布订阅-代理
服务代理还带来另一个好处。被代理的软件模块被隐藏在代理服务后面,可以单独进化。比如采用不同的技术路线网络总线重新实现。
但是服务代理作为额外的间接层会降低效率和用户可察觉的性能。所以服务代理暴露出哪些原有软件模块的功能需要仔细选择。比如,被代理的模块是一个实时性要求很高动力系统ECU,那就没必要把该ECU的高实时要求的控制信号暴露出来,只应该暴露出实时性要求不高的状态发布的等信息接口。
3.3.6 服务装配
在进行服务划分的时候,我们希望把每个服务设计得尽可能功能单一,这样服务简单,方便开发、测试和复用。但是会造成服务数量变大。
在服务可以执行之前,它必须被加载到应用程序(操作系统进程)的地址空间中。如果每个服务一个进程,如果有上百个服务,就会造成操作系统中运行着上百个进程,争抢系统资源。相当与把服务调度的工作交给了操作系统,让操作系统的进程调度代为执行服务的调度。我们知道,操作系统的进程切换是开销非常大的操作,也无法保证调度的精确性。比如,我们希望一个服务每秒钟执行30次(软实时),当有上百个繁忙的进程在系统中执行时,操作系统的进程调度策略是无法保证这个服务的调度要求的。
我们可以把多个相关的服务装配到同一个服务容器进程中,由服务容器来对这些服务进行调度。这样可以在用户空间而不是内核空间进行服务切换,避免了大部分进程切换的系统开销。同时可以自定义调度算法以满足服务的需要的调度要求。
如果服务装配策略(哪些服务装配到一个进程里)是在开发早期就做出了决策,这个时间开发人员往往并不知道服务搭配或部署的最佳方式,一旦决策有误,再变更难度就很大。此外,对“最佳”配置的定义,很可能会随着计算环境的变化而变化。
如果服务的实现与其初始配置紧密耦合,则修改服务可能会对其它服务产生不利影响,比如会导致其它服务需要被重新编译和部署。
解决问题较好的办法是动态服务装配机制。每个服务开发时并不是被预设为单独的进程,而是一个可以被动态加载的模块。(如:动态链接库Windows
上的DLL或Linux 上的 SO 文件)。在服务部署时才决定哪些服务被装配到同一个进程中。也可以在运行时才根据需要的加载服务,并在利用完成后卸载。甚至可以让服务在不同进程、不同操作系统中迁移(从一个进程中卸载,在另一个进程中装载)。
图3. 11客户-服务器-状态分布-服务发现-发布订阅-代理-装配
每个服务声明自己的调度要求(执行的频次,要求完成的时间等),由服务容器的调度算法来满足,不能满足时也能获知并收到告警。图3.
11将服务装配加入了我们的SOA架构风格。
“服务容器”本身也可以被设计成SOA服务,提供服务管理接口,用于加载、管理其它服务。所以图中“服务容器”继承自“服务器”,多个“服务器”又可以装配到“服务容器”。
从“关注点分离”的角度看,服务装配分离了“服务实现”与“服务部署”两个关注点。“服务实现”时优先关注功能的定义与实现,而部署决策可以被延迟指定。
服务装配还带来另一个优点,就是为服务之间的数据交互提供了优化的空间。虽然我们默认采用通过网络进行数据交换。但是当两个服务部署在一个进程内时,显然有更合适的数据交换通道。后文会进一步讨论这个问题。
3.3.7 服务监督
对系统中的服务进行监督管理是必要的。比如,Linux 系统的系统管理守护进程“Systemd”就是用于对Linux
系统服务进行监督管理。它会根据预定的配置在合适时间启动服务,并监督服务进程,如果进程消失,会自动重新启动。Systemctl
命令就是用来与 Systemd 服务进行交互的命令行接口。
对SOA服务而言,我们需要对服务的“生命周期”、服务的“健康状态”还有“服务质量”进行监督管理。
图3. 12 SOA:客户-服务器-状态分布-服务发现-发布订阅-代理-装配-监督
管理服务的“生命周期”是指要决定什么时候加载、启动服务,什么时候关闭、卸载服务。当系统中只有少数服务的时候这个问题可能不是很严重,简单的系统就是启动时所有服务都起来。但是当部署了几十上百个互相依赖的服务后,服务的“生命周期”问题就很重要了。尤其车载ECU需要对功耗进行控制,当前场景不需要的服务应该尽可能不启用。比如,泊车场景需要使用环视摄像头的图像识别车位线,当判断车辆行驶在高速公路上是,车位线识别的算法服务显然没必要加载。当车辆到达导航的目的地附近时,车位线识别服务可以被预先加载,但是不激活(执行算法识别),当用户启用泊车功能时,算法服务开始工作,泊车过程结束,算法服务停止计算。
服务“健康状态”的监督包括确定服务是否异常退出,服务所依赖的资源是否不可用而导致服务状态不正确。尤其是多个服务相互依赖时,服务的“健康状态”问题会顺着依赖链进行传播。在车载系统中,这跟故障诊断和功能安全密切相关。
即便服务在持续工作,但是它的“服务质量”是否满足要求也是需要被监督的。服务质量包括其响应时间,系统资源占用等等。对于检测出来的问题,“监督服务”需要决定处置措施,比如:重启、告警、功能降级等等。
图3. 12是在我们的SOA架构风格中增加的“服务监督”风格。监督服务本身也是一个SOA服务,只是有自己定义的标准化服务监督接口。所以图中“监督服务”继承自“服务器”。监督服务与服务容器和其它SOA服务通过监督接口进行交互。
3.3.8 RESTful API
对于互联网行业的开发人员来说,REST 以及 RESTful API 是司空见惯的事情。REST设计风格以及基于REST的HTTP协议是互联网软件架构基础。REST
是一个庞大话题,参考资料[23]中有详述,这里不多介绍。RESTful API 是基于REST 的原理,基于Http
协议实现的API 设计原则,遵循这些原则,可以设计出清晰简洁易于维护的API接口。更为重要的是,各种异构系统,如果能够对外提供基于
HTTP 实现的RESTful API,就可以在更大范围内做应用系统的集成。比如各大云服务提供商(AWS,阿里,百度等),都为它们的各种云基础设施提供了
RESTful API接口,我们就可以很方便的使用程序去管理我们的云端资源(如创建一台云主机,读取或更新数据库)。
图3. 13 SOA:客户-服务器-状态分布-服务发现-发布订阅-代理-装配-监督-Restful
现代智能网联汽车会与互联网有非常多的数据交互,这些交互不像车内通讯要求有很高的实时性,但是外部系统确有复杂的多样性,我们可以为某些服务提供RESTful
API,以便能更好的与外部系统集成。图3. 12在 SOA架构风格中增加了RESTful API 约束。
RESTful API 设计有一些指导原则,可以参见 MicrosoftREST API Guidelines。这里做一些简要的说明。
设计RESTful API 首先要做好URI 的规划,需要把服务中的概念映射成合适的URI。比如我们给娱乐系统的音量设计一个
URI 来表示,那就可以对这个 URI使用 HTTP 的GET 和 PUT 方法来读取和设置音量值。
HTTP协议的操作方法很少,只有9个。RESTful API 最常用的方法主要是 GET、PUT、POST、DELETE,使用这几个方法就可以完成常见的CURD操作(create,update,read,
delete)。这些操作方法如何映射到 SOA 服务的方法有一些基本的原则,这里结合SOME/IP
来做一些说明。
HTTP 的GET方法有一个要求,就是它不应该改变被调用的服务的状态,它只是读取一个URI的值,而不会改变它。PUT
方法有一个特点,其任意多次执行所产生的影响均与一次执行的影响相同,数学上管这个叫“幂等”(GET/PUT/DELETE
方法都是“幂等”的,但只有 GET不改变状态)。SOME/IP 协议中与之对应有同样特性的是Field。所以对于服务中的
Field 字段应该映射为 RESTful 的GET/PUT 方法进行操作。
SOME/IP 中的 Method 应该映射为对某个 URI 的POST 操作。RESTful推荐用良好的
URI 规划来更准确的表达领域的语义。所以比较合适的方式是每个Method有其URI,Method的参数和返回值体现在
POST 方法的提交数据和响应结果上。
SOME/IP 的 Event 映射为RESTful API 时比较麻烦,因为HTTP1.1协议是不支持向客户端主动通知的,不过有变通的WebSocket
方案,HTTP/2 是可以支持的,都需要由客户端向服务器发起GET 请求,服务器有需要向下通知的数据时就返回数据内容。
这些就是SOA 服务转换为 RESTfulAPI 的基本映射方式。一般来说并不需要为所有服务设计RESTful
API,只为需要与外部系统集成的服务提供RESTful API即可。
3.3.8 可选的其它风格
“虚拟机/解释器”风格
这里的“虚拟机”指的是受控的代码执行环境,比如 JavaScript 虚拟机,Lua脚本解释器等。服务器向客户端下发一段代码,客户端在严格受控的执行环境中执行代码。这个受控的环境只能访问指定的资源,对资源的访问权限被限制在预定义的范围内。
对车载应用来说,对这种方式的需求往往出现在与云端有交互的场景。因为“虚拟器/解释器”可以先部署到车上,易变的需求可以后续由云端下发代码来满足,这在车载娱乐系统中会很常见。我们举一个为自动驾驶服务的数据采集场景来说明。
自动驾驶的很多算法以及测试场景非常依赖对数据的收集,相对于专业的采集车,量产汽车可以提供更为真实的数据案例,更广的覆盖范围。采集并上传哪些数据需要一些规则进行控制,否则没有针对性的大量数据上传会对带宽占用、数据存储、数据分析带来不利的影响。
可以在车辆量产时内置数据采集和上传的能力,以及检查采集规则的规则引擎。具体的采集规则由云端根据需要下发。比如视觉算法需要改进对雨雾天气的识别效果,就对出现雨雾天气的区域车辆下发采集规则的更新。车辆数据采集服务接收规则本地执行,触发数据采集事件。这样采集的数据内容可以根据需要随时调整,带来了较好的灵活性。这时规则引擎就相当与一个受限的解释器,下发的规则内容就是被执行的代码。
“远程求值”风格
“远程求值”风格跟“虚拟机/解释器”风格正好相反,是客户端把代码送到服务端执行。同样,这种方式的需求也出现在与云端有交互的场景。之所以把代码送到服务端执行,是因为执行所需要的数据在服务端。这些数据或者是因为数据量大不便传输,或者是因为数据安全或数据隐私的原因,不能被下发给客户端。客户端可以将代码发送到服务端执行,利用数据,取回结果。
这种方式在智能网联汽车的“车路云协同”上是有应用场景的。根据需要,联网的路侧单元至少可以保存道路沿线一定距离内的道路、车辆等信息,云端的服务器可以保存更大范围内的交通状况数据。这些数据都不方便直接发送给行驶中的车辆。当然路侧单元和云端服务器都可以根据自己保存的数据提供一些预定义的服务,供车端调用。但是更灵活的方式,是开放执行环境,由车端上传代码来决定如何利用数据。当然被执行代码的权限也会被限制,执行环境也会是一个受限的沙箱。
这种方式优点是能够定制服务器组件的服务,这改善了可扩展性和可定制性;代码直接在服务端执行,减少了服务器与客户端的交互能够得到更好的效率。由于客户端发送代码而不是标准化的查询,因此缺乏可见性。服务器如何信任客户端,如何控制执行环境的安全性也需要考虑。这会对服务的部署带来难度。
3.3.9 小结
这一节通过对SOA架构风格的推导,阐述了车载软件的SOA 风格并不是一个单一的架构风格,是一系列软件架构风格的组合。
对于车载软件,我们首先考虑的是如何降低其复杂性,划分为依赖性尽可能小的多个服务,是一种化整为零的方法。为了让服务尽可能简单,需要考虑服务的状态分布,强状态依赖的功能集中在特定服务,让其它服务以尽量以无状态的方式设计,以利于整体系统的开发、测试、复用。服务发现用来简化大量服务的配置,基于事件的发布订阅让服务之间的通讯偶合性降低。服务装配用于更好管理服务的部署,服务监督让服务的可靠性得到保障。RESRful
API 增强车内服务与外部系统的互操作性。
这些软件架构风格很多都是在各个领域得到了广泛应用,以各种不同的形式存在。针对特定的应用场景,选择不同风格的组合,发挥各自的优势,往往能产生1+1>2
的效果。
3.4 SOA 的架构元素
前文3.1.1节提到,软件架构由三个方面组成,架构元素,架构的组成形式,和一些形成架构的基本原则。前文SOA推导时每一步都对此三个方面有一些体现。这里我们再从整体上来对架构元素的区分以及其组成形式做一些分析。
架构元素分为“处理元素”、“数据元素”、“连接元素”。
SOA 的“数据元素”比较容易识别,无论是RPC调用还是消息的发布订阅,都是数据在不同服务之间传递。深入理解这一点最好的方式是与面向对象的分布式系统做对比。面向对象的核心概念之一是“封装”,其关键含义在于将数据以及操作数据的方法封装在对象实例中,对象私有数据对外不可见。这可以理解为将“数据元素”与“处理元素”封装在了一起。面向对象的软件设计就更关注如何将领域概念转换成合适的对象模型,定义对象的行为和操作,以及对象之间的组成结构。
与面向对象的方法对比,SOA 架构中“数据元素”和“处理元素”的耦合度就低很多。基于消息的发布订阅完全是从数据的视角看世界,基本不关心数据如何被处理,只关注数据之间的供需关系。RPC请求可以被理解为“一对一”的数据供给与需求关系。尤其在无状态服务中,多个RPC请求之前没有状态上的关联性,SOA服务的数据处理能力就更为“纯粹”(函数式编程的中的概念,也称作无副作用)。大型系统在设计时,考虑问题的视角就是如何寻找合适的数据边界以界定服务边界,而不是对领域进行对象建模,这与分布式对象系统是完全不同的设计理念。与分布式对象的对比,在3.5.1节中还有进一步的讨论。
开发SOA架构中的“处理元素”是某个具体SOA服务的开发人员的职责,我们希望开发人员专注于这个具体数据处理的实现,比如某个AI算法,某个数据集的MapReduce过程。而数据从哪来,到哪去,开发人员不需要关心,这由SOA的“连接元素”来负责。
在实际工程实践中,SOA服务的开发者并不会完成全部的从数据处理到数据通讯的全部工作,而是要借助分布式中间件系统来实现。中间件提供Runtime库,IDL规范,程序语言特定的代码生成工具。使用IDL定义出数据通讯协议后,再用工具根据IDL生成代码。用户编写“处理元素”,使用IDL生成的代码与外部通讯。这时候,IDL生成的代码和中间件Runtime
就扮演了“连接元素”的角色。它从通讯通道接收数据传递给“处理元素”,将处理的结果发送给需求者。
一个SOA系统在整体设计时,架构师要关注“数据元素的定义”,“处理元素”的划分, 以及选择合适的“连接元素”将它们组合在起来。但当将某个明确的数据处理要求委托给某个团队(如:内部的某个算法团队,或者外部的供应商)开发时,架构师希望连接元素对与该团队是透明的。也就是说数据的处理不应该依赖于特定的连接方式。架构师甚至希望只需要提供给开发团队模拟的连接方式并回放预先录制的数据,开发团队基于这些来实现其数据“处理元素”。“处理元素”的完成品可以被架构师集成到真实的应用环境中,那时使用的可能是不同的连接元素。
中间件Runtime能够使用SOME/IP、DDS、共享内存等各种不同连接通道;工具根据IDL生成的代码完成用户代码和中间件Runtime的连接;服务发现让服务的位置不是固定的而是可以被配置、可动态发现的,这些都是在发挥“连接元素”的作用。
从“关注点分离”的视角看,SOA架构在三类主要的架构元素上实现了关注点分离,这也是它适合用于复杂系统集成的重要原因之一。
3.5 SOA相关其它问题讨论
3.5.1 SOA vs 分布式对象
分析 SOA,如果跟分布式对象做一些比较,可以更好的理解SOA的意义。我们在3.2 节提到的架构风格中有“分布式对象”风格。前文也提到CORBA
和 ZeroC ICE 都是分布对象的实现。
SOA 和分布式对象都是分布式网络架构的实现形式。只不过一个是 Service Oriented
,一个是 Object-Oriented。我们来看看他们有什么异同,为什么车载软件选择 Service-Oriented
而不是Object-Oriented。
面向对象(Object-Oriented)是非常重要的软件设计思想。当软件规模进一步复杂后,“结构化编程”方法也体现出一定的不足。面向对象的设计思想是解决这些问题的方法论之一。从C++开始,大多数程序设计语言都支持面向对象的方法论。它把数据和处理数据的方法封装在一个对象中,可以用来与具体的现实事物或抽象的语义概念进行对应。提供了对现实问题或语义概念进行建模的可能,用来描述复杂的软件语义。
程序语言创建的对象是本地对象,即访问对象的内部数据和接口方法都是当前进程内的行为,不涉及网络通讯。当面向对象的设计理念在本地编程获得成功后,人们很自然的会想到,在分布式领域中是不是可以使用类似的方法。一个对象封装了数据和操作方法,部署在某个服务器上,客户程序通过网络进行访问。在客户端也提供本地化的API接口,使用这些API时跟访问本地接口一样,但是请求会被自动代理到服务器上的某个对象。这就是分布式对象的由来。
CORBA 标准建立之初,人们曾经认为这将是未来主流的分布式技术。但实际上世界上最大的分布式系统万维网(WWW),并没有采用分布式对象。车载软件的分布式化选择了SOA,也没有选择分布式对象。我们从几个角度来探究其背后的原因。
强“状态相关”的服务不利于可扩展性
3.3.2 节讨论了程序状态在客户端和服务器端的分布情况对软件架构的影响。我们倾向于将服务设计成无状态的。这在WWW
的架构中尤为重要,这是WWW 世界能成为超大规模系统的关键原因之一。
分布式对象将数据与操作接口封装在一起,也意味着它将状态留在了服务器端。从这种意义上看分布式对象架构风格与远程会话风格有点接近,每一个远程的分布式对象自身就是一个小的会话。对于同一个远程对象的多次方法调用,都要精准的找到这个对象所在的位置。
对于 WWW 来说,这种方式会严重影响其可伸缩性。WWW 中,每一次请求的无状态特性可以让一个负载集群系统中的任何一个空闲的服务器都可以处理任何请求,而不需要一定让多个请求必须绑定到同一个服务器。
对于车载软件来说,虽然整体系统很复杂,但是到单个具体的服务点,其功能往往是很单一的,处理逻辑的复杂度远远小于电子商务等系统。很多时候就是接收数据,做简单处理,然后发送数据。面向对象的建模对这些简单功能来说带来了不必要的复杂度。无状态的服务设计方式能让系统大大简化。
不同对象的接口差异大
面向对象的设计方法很重要一点是对行业领域进行建模,分析出各种对象类型以及它们之间的关系。不同类型的对象就包含不同的状态数据以及不同的操作接口。
对象类型多、接口各不相同对于开发本地应用来说不是大问题,因为一般应用程序也就几十的对象类型,即便几百个也是在可以处理的数量级内。但是对于WWW
系统,可能会面临几千万甚至更高数量级的对象类型,没人能处理这么复杂的系统互操作。
WWW 采用的 REST 架构的一个关键设计约束是统一接口,实际只有GET, PUT, POST,
DELETE等有限几个操作方法。仅仅这几个操作方法就能完成 WWW上各种复杂的功能,非常的神奇。奥妙在于WWW
使用URI(统一资源标识)重新定义世界。WWW上的每一个事物(简单的文件、图片;或者订单、人等各种语义概念)都可以使用一个
URI去表示。世界的复杂性从对象模型的关系,转换到了树形的URI表示,从而采用简单的操作方法完成所有可能的操作需求,如果不能满足,就再拆解出一个合适的URI形式。
对于车载软件来说,服务的类型数量也是在一个可控的数量级,并不需要URI机制去简化接口形式。但是如前文所说,当汽车软件需要与云端或者互联网体系进行数据交换时,可以把车载软件的部分服务包装成RESTful
接口遵循WWW系统的架构风格。
对象生命周期管理复杂
无论是本地对象还是分布式远程对象,都会有一创建、初始化、工作、失效、销毁的完整生命周期。对于分布式对象来说,其生命周期管理非常复杂。比如,当一个客户端请求某个对象的服务是,这个对象可能不存在,那么这种情况下如何处理也需要被考虑。
一般来说这种对象生命周期管理能力是由中间件系统来提供,CORBA,J2EE规范中都有对应的内容。会导致中间件实现的复杂度。
分布式对象系统,当然是希望以统一的方式管理大量的对象,比如成千上万的订单。只有这样才值得在开发复杂中间件实现时的投入。然而对于车载软件,很多软件模块恐怕只有一个对象需要被管理,比如上文的ACC
会话。一辆车同一时间内是不可能产生两个及以上ACC会话的。即便某些服务需要多个会话实例(相当于要管理多个对象的生命周期),那这个复杂性由服务自身去处理。
比如某个服务内部确实需要保存多个不同对象(或会话)的实例,那么可以提供类似“createXXX”的接口并返回对象的句柄,然后在其它的接口方法参数中带上这个句柄。服务实现时根据这个传入的句柄去找到对应的对象进行操作。至于对象的生命周期,服务开发者自己想办法维护。而不需要在中间件这一层提供架构级别的解决方案,因为投入与收获不匹配,并且带来不必要的复杂度。
3.5.2 RPC\消息通讯\管道 的技术关联
RPC 是“客户-服务器”架构风格实现请求响应的典型方式。“基于消息的通讯”(或者叫基于事件的集成、发布订阅模式)及“管道和过滤器”本身也是常用的架构风格。这三者是架构组件之间数据通讯的方式。从架构风格的组件耦合程度看,RPC
耦合度最高,管道模式次之,发布订阅耦合度最低。但在具体实现上,这三者之间很大程度上可以互为实现。
基于RPC 实现发布/订阅
如果我们实现了“客户-服务器”之间的RPC 机制,我们可以利用它来构建发布订阅系统。ZeroC ICE
支持的发布订阅服务IceStorm就是这么实现的。但是这个发布订阅是通过中心节点进行中转的。
图3. 14基于RPC 实现发布/订阅
如图3. 14所示,“接口A”定义了一个 RPC 接口,在普通的基于RPC的“客户-服务器”系统中,客户端直接调用接口A,服务器获取数据进行处理。当转换成“发布/订阅”模式时,引入中转服务:
1、中转服务提供基于主题(Topic,可以是一个字符串名称)的Publish和 Subscribe
接口。中转服务对每一个主题维护订阅者列表。
2、订阅者通过调用中转服务的Subscribe接口将自己注册到中转服务中,以订阅特定主题。实质上就是告诉中转服务自己对哪个主题感兴趣,告诉它回调自己的访问点(IP,端口等)。
3、发布者按主题发布数据,实际是对接口A的一次RPC调用,但是带上了主题名称,方便中转服务识别。
4、中转服务并不识别并执行发布者的请求,只是根据主题名称将请求转发给订阅者。接口的适配(参数定义,版本匹配)由发布者和订阅者自己保证。
以上实现“发布/定义”方式的缺点是有中转服务,一方面存在单点失败的可能,一方面两次数据传输有额外的网络性能开销。另外只适合没有返回值的RPC调用。优点也很明显,可以方便的把
RPC 服务转换成“发布/订阅”模式,能够方便的达到解耦和发布者和订阅者的目的。中转服务是与特定接口无关的,也就是说任何RPC接口都可以这么转换。因为中转服务不需要识别接口内容,只是单纯的做数据报文的转发,不做序列化和反序列化动作,所以可以适用于任何单向RPC接口。
基于发布订阅实现RPC
反过来,我们也可以基于“发布/订阅”的消息通讯实现 RPC机制。“发布/订阅”逻辑上是实现一个“多播”机制。多个软件组件可以订阅某一个主题的消息,不关心谁发送;多个组件也可以发出某个主题的消息而不关心谁会接收。既然可以“多播”,当然也可以“单播”,我们完全可以给用户层一个RPC的API,而在实现的时候把RPC调用转化成单播的消息通讯,比如利用DDS来实现。
基于“发布/订阅”来实现管道
在管道和过滤器风格中,每个组件(过滤器)从其输入端读取数据流,对数据进行处理后在其输出端产生数据流。每个过滤器必须完全独立于其它的过滤器(零耦合),它不能与其它过滤器在其上行和下行数据流接口分享状态、控制线程或其它资源。“管道和过滤器”有非常好的可配置性,可扩展性。
如果我们把管道中每一个过滤器的输入和输出数据流定义成“发布/订阅”的消息,那么也可以实现管道和过滤器的风格形式。实际很多基于ROS/ROS2
实现的系统就是这么用的。
----
以上几个是架构风格在实现层面是交叉支持的具体形式,其实还可以有更多。如前文所述,架构风格定义的是软件组件之间结构关系的约束形式。每一个架构风格当然有其最优的原生实现形式,但是也不排除在具体实现技术上基于其它风格实现。
3.5.3车载以太网助力 SOA架构
SOA 在分布式系统中实际上已经被应用很多。不过在以太网用于汽车之前,车载软件其实是不怎么提 SOA的。SOA的相关设计约束并不是说一定要基于以太网,只是在车载软件上,以太网能让SOA
更好的实现并发挥作用。对此我们做一些说明。
先明确讨论中“以太网”的概念。当我们谈及“以太网”的时候,根据上下文其实往往有“狭义”和“广义”两种含义。
狭义的以太网重点关注的是以太网的物理层和链路层。这时候我们指的以太网是符合 100BASE-T、1000BASE-T等标准的有线网络,采用总线型拓扑,或者基于交换机实现星型拓扑。使用CSMA/CD(Carrier
Sense Multiple Access/Collision Detection,即载波多重访问/碰撞侦测)的总线技术来解决通讯冲突。在这个语义下,WiFi
不是以太网,它是无线通讯技术,有另一套标准(IEEE 802.11)。
广义的“以太网”包含了常用的通讯协议,最核心的是对IP 层协议的支持。ISO/OSI 定义的七层网络模型中,物理层和链路层之上是
IP 层(网络层)。传输层协议(TCP,UDP)也都是基于 IP 层。IP成定义了数据报文进行地址和传输的协议,无论下面两层如何实现,只要有IP层的支持,不同网络就是互通的。在这个语义下,WiFi
也是以太网,因为它也支持 IP 协议。我们还可以实现 IP over USB, 那就是基于 USB
的以太网。
下面讨论中的“以太网”指的是广义的以太网,即具有IP协议支持的网络。
Can 总线在汽车中的到广泛的运用,Can总线只有物理层和数据链路层([27]3.3)。使用 Can
总线的应用需要在这个基础的数据链路层协议上去定义自己的数据格式,一般以一个DBC 格式文件描述。Lin总线成本更低,
FlexRay 总线提供了比 Can 高得多的数据传输带宽,但是他们也跟 Can 总线一样,只有物理层和数据链路层,应用层协议各自为政。这意味着这几个网络是无法互通的。汽车电子电器架构设计的时候,解决互通问题的办法就是两个网络中间加网关。网关同时支持两个以上网络并来回搬运数据。即使是两条Can
总线想要互通也需要加网关,而且网关的数据搬运代码都需要单独定制,因为每条Can的应用层协议都不一样。
设想一下,在这种网络环境下做SOA服务是什么效果。假如我们基于 Can 协议实现了一个 SOA 服务,我们没法进行RPC请求,因为
Can 协议没有寻址的概念,请求不知道发给谁。不过好消息是Can 本质上也是发布订阅的机制,我们可以放弃单播通讯,只做事件广播。但
Can 一个消息只有8个字节,没有地方放更多的头信息,我们在设计服务发现机制的时候会遇到巨大挑战。就算我们设计出了服务发现,对不起,这个服务在别的网段中不会被发现,因为各个网段的服务是不能互通的。我们就需要开发网关程序跨网段搬运数据。但是每增加一个服务,或者对服务的数据格式做些修改,网关程序都要重新修改适配。就算这些都完成了,我们想让一个开发好的服务在另一个项目中复用几乎不可能,因为对应的Can
协议,网关程序全部都要重新适配。
引入以太网技术带来的 IP 层是解决这些问题的关键。不管各段网络的物理层和链路层是什么样的,只要支持
IP层协议,IP报文就可以在不同网段之间传输。IP 协议是可以支持广播和多播(一次数据发送,多个目标接收)的。而且广播和多播是可以跨网段的,有成熟的协议支持。广播和多播可被用于SOA
的“服务发现”和服务之间的数据发布订阅。
以太网比大多数其它车载网络提供了更高的带宽,目前常用的车载以太网系统基本都可以达到1000Mbps。将来升级到万兆甚至更高的光纤以太网也是指日可待。而这种升级在软件架构上几乎不需要做太多变化就可以利用网速提高带来的好处。这样在数据报文设计上就不用像设计Can报文一样精打细算的利用好每一个
bit。必要的情况下可以增加更多的头信息支持更多的功能。也可以用来传输图像等多媒体数据。扩大了SOA的服务能力范围。
TCP/IP 协议发展了很多年,是互联网的技术基础。衍生出了无数成熟的网络技术,这些都可以适当调整后运用到车载软件。比如基于发布订阅的DDS技术,提供了丰富的
QoS能力,可以用于支持服务组件之间的通讯;与外部系统集成可以使用基于HTTP相关的技术。这些技术的有
效利用可以很快的提供更多丰富的功能。
引入以太网也有一些其它问题需要解决。以太网的工作模式就是多个联网节点上的多个应用争抢网络带宽。某个应用的高带宽占用可能会导致另一个应用的紧急数据不能及时传输,影响车内服务之间通讯的实时性。TSN
(时间敏感网络)相关协议的就是用来解决这些问题。
总而言之,在车载软件中,以太网技术是支持车载SOA架构风格的关键技术基础。
3.5.4 SOA 与 微服务
在汽车软件开始向 SOA 架构转型时,大型互联网服务基本上都已经转向了微服务架构风格。那么,SOA与微服务的异同点是什么?汽车软件也会走向微服务架构吗?
规模引起的量变到质变
首先SOA与微服务在架构风格的逻辑上是一脉相承的,前文对SOA的论述对微服务同样有效。但是微服务在架构风格上更进一步,可以理解为至少增加了以下几个约束:
1、服务粒度尽可能小
2、服务之间的依赖性更小
3、更彻底的去中心化
服务粒度尽可能小可以理解为比SOA更小的服务拆分,强调的重点是业务系统彻底的组件化和服务化,原有的单个业务系统会拆分为多个可以独立开发,设计,运行和运维的小应用,这些小应用之间通过服务完成交互和集成。每个服务完全不依赖中心化的资源,比如不依赖中心化的数据库,每个服务有自己的数据存储机制。粒度越小、相互之间依赖越小,无中心化,让开发、测试、部署的独立性越强,越容易使用负载均衡技术支持高度的并发访问。
相对于SOA ,微服务是一个量变引起质变的过程。目的是为了支持更大型的互联网服务体系,应对高并发、高可用要求、高业务复杂性的挑战,同时要求开发迭代更为敏捷迅速,部署简单并高度自动化。像电商的秒杀系统,12306
购票系统,都是极限并发的典型案例,微服务架构在应对这些场景的时候有很好的表现。
微服务可以认为是SOA架构向更深度的发展进化。但是互联网的微服务架构和车载的SOA架构,应用场景有很大的差别。虽然汽车软件也是分布式架构,而且车载以太网技术得到应用后,很多车载分布式技术与互联网的分布式技术有非常多的共通之处。但是车载软件的分布式规模与互联网服务完全不在一个量级。
“分布”与“集中”
大型互联网服务部署规模可能涉及上百台服务器,每秒百万级的并发。车载的分布式服务只在一台车的局部系统中,并发要求低但实时性要求高。与互联网的极度分布式趋势不同,车载系统是反过来,目前是向集中式发展。原来电子电气架构中大量小控制器实现的功能,逐步向几个主要的高性能域控制器集中。
车载SOA架构在功能划分上尽量切分成多个服务,但是部署的时候实际是集中化的,同时通过一系列技术手段优化通讯延迟。有意思的是,这个“集中”其实是与“分布式”并存的。
图3. 15物理的集中与逻辑的分布
图3. 15是一个理想的域控制器内的服务部署。高性能的多核心SoC被虚拟化成了多个虚拟机,VM1和VM2分别是Linux和QNX系统,而且都支持容器化(Docker
Enable)。每个虚拟机内又有多个容器(Docker Container,不是前文服务装配中的服务容器
)。每个容器是一个独立的 OS系统,里面再部署SOA服务进程,进程内有多个SOA服务。不同虚拟机内、不同容器内的服务通过网络进行通讯。
我们可以看到,整体的域控制器中的软件在物理上是“集中”部署到了同一个域控制器主机,但是逻辑上又“分布”到了不同的操作系统实例。这样的好处是可以使用虚拟机或Docker容器作为供应商的边界。一个供应商提供一个虚拟机或容器内的全部服务,与其它供应商互不影响,责任边界也容易确定。为了优化通讯我们可以在虚拟化这一层提供PCIe总线的虚拟化来支持跨虚拟机的共享内存通讯。
虚拟化技术和容器技术在云端计算中心已经被大规模使用,车载系统只是在复刻这个过程。差别仍然在于规模,现实应用中微服务架构基于的虚拟机和容器数量比车载系统至少高出两个数量级。而且虚拟化对微服务架构是完全透明的,微服务架构中的服务认为自己运行在独立的OS中,在微服务架构中,只有极度的“分布式”,没有“集中”部署的含义。
技术实现上的侧重点不同
两者的应用场景不同决定了分布式规模不同,导致在具体的设计和实现上的侧重点也有很大差别。
车载SOA首要解决的是服务能够被拆分,拆分之后能够通讯,然后解决如何优化通讯的效率,尤其是一定的实时性保证。
微服务架构因为彻底的分布式带来的大量微服务组件,需要在更大的规模上去解决“负载均衡、服务发现、认证授权、监控追踪、流量控制、服务部署、分布式事务、分布式存储”等等问题。以目前最先进的微服务架构Service
Mesh来说,2017 年1月发布的Service Mesh产品Linkerd,所有的请求都通过 Service
Mesh 转发,不提供直连方式,它掌控所有的流量。2017 年 5 月, Google、IBM、Lyft
联手发布了 Istio,它与第一代 Service Mesh 相比,增加了控制平面,它具备远超第一代的控制能力。通过集中式控制面板,加上所有流量均会通过
Service Mesh 转发,通过 Service Mesh 的控制面板,就可以控制所有整个系统。Service
Mesh管控的内容出来服务注册和服务发现外,还包括负载均衡机制,弹性的流量控制能力(熔断、限流、降级、超时、重试等)。而转发引起的消息延迟在互联网业务中并不是最重要的关注点。
车载软件架构会走向“微服务”吗
车载SOA架构是实际上跟互联网技术上的SOA架构也是有很大差别的,加入了很多为车载场景定制的协议和优化机制。微服务架构作为SOA的进一步演进,已经在互联网领域体现了其价值所在。车载的SOA架构也不会一成不变,也会进一步的演化,很自然的也会从微服务架构中吸取优秀思想。但不会是直接使用互联网微服务的产品。
目前来说车载软件的当务之急还是先实现在业务功能层面SOA化,然后在服务部署上应用虚拟化和容器化,以支持不同粒度上的服务部署,并建立供应商的责任边界。进一步的改进可以根据现实问题来做响应。有一个现成微服务架构作为参照,也为后续架构改进的思路提供了技术储备。 |