67

从应用层表象出发进行 IoT 架构抽象设计

 5 years ago
source link: http://www.ibm.com/developerworks/cn/iot/library/iot-lo-architecture-design-from-application-layer/index.html?ca=drs-&%3Butm_source=tuicool&%3Butm_medium=referral
Go to the source link to view the article. You can view the picture content, updated content and better typesetting reading experience. If the link is broken, please click the button below to view the snapshot at that time.

前言

本文旨在说明一种数据定义的规则标准,可以用来抽象描述绝大部分类目的物联网设备。本定义体系可以在一套中间语言的语法下,极好的解耦设备接入服务和设备应用服务,从而可以天然完成下述诱人的特性支持:

  • 新硬件接入服务、新应用接入服务的热插拔式服务开发
  • 同类硬件的服务故障传播阻断
  • 能够原子化自由组合的中台服务方式
  • 针对触发、功能类操作的全被动埋点支持
  • 前向兼容的升级扩展服务支持
  • 算法、数据友好型 IoT 数据池模型
  • 简单可行的自动化数据字典文档生成支持

本文主要描述数据抽象原则与方法论,通过 IoT 系统的背景介绍、设计挑战说明设计本方法的初衷,然后通过五元组和从属理论基础的引入描述语言的形态以及对应实现,至于通过架构技术实践参考,后续有时间将通过独立的文章来进行描述。

IoT 系统模型的挑战

海量场景下的多样性

IoT 应用的数量、种类近几年在资本、更成熟的技术、更低价的网络环境等利好因素的催生下开始井喷。但是行业发展速度过快,导致的一个后果就是行业内并没有足够的时间来沉淀、淘洗通用的应用模型框架,各种差异巨大但是又各有所长的物联网终端构成了繁荣但又无序的 IoT 生态。生态行业之广,囊括工业自动化、消费电子、市政、汽车、安防等,而网络协议上又包括 MQTT、HTTP、三方平台以及各种五花八门的自定义 TCP/UDP 数据交互流协议,即使对应用方来讲的同一个设备,不同供应商不同型号的产品也大概率会有非常大的实现差异。

由于技术实现方案、供应商能力的差异,不同设备的升级、维护、安全以及故障概率都会有很大差异,而且差异是动态发生变更的。尤其是针对同一类产品,考虑规避风险、降低成本等因素从而涉及到多供应商多型号产品的应用系统,这些问题就会被进一步放大。

深度耦合的应用模型

如果考虑到紧张的研发周期、快速的需求迭代周期和有限的人力等因素,那在各种适配定制开发的过程中,几乎无法避免的会引入应用服务、设备接入过度耦合、不同型号设备技术实现混杂等问题。而这些必然会产生的问题,在某款设备出现故障、一款设备的非同步升级迭代、应用需求的差异化变更等情境下,就会进一步放大,以至于持续性的修补而给系统带来更多对稳定的不确定性影响。

另一方面,针对调用和状态切换行为侦测、数据池的数据结构化抽取等进一步应用,这种强耦合的实现模式就需要定制大量的代码,从而独立实现各种不同的服务接口。且由于开发时间的紧张,往往没有时间在项目完成时交付一个最新的技术同步文档,更加做不到跟随产品的每次更新迭代同步更新所有的文档说明。

本文就是想寻求一种方式,隔绝系统耦合,排除设备差异性的干扰,对应用暴露单一的设备抽象描述方法约定,针对不同的设备,不管是什么类型的协议以及是否有定制的能力,都可以顺利接入。尽管用一种描述方式来覆盖绝大多数的物联网设备应用,听起来太过理想化,但是确实是存在着这种方案,接下来我们会从所有物联网设备的隐含共性特征出发,一步步勾勒这种描述框架的外貌与内里。

一些显而易见的前提

共性前提

既然寻找统一抽象方法,第一步自然就需要寻找大部分(至少是需要覆盖到的)对象的共性前提,然后再根据这些前提提取对象的特征,并选择一种合适且实现无关的描述方法对其进行适配以及属性填充。然后通过粒度是否已小到最小原子操作和最小的维度数据采集来确认描述方式是否合理。

针对物联网应用来讲,会有下述几个关键可利用前提:

  • 所有的操作(即使是批量)一定是针对每一个设备的独立操作,即不同于互联网应用的批量操作,每一个设备的返回结果考虑到环境、设备状态等变量因素影响,返回的时间和数据都是不可预知的。
  • 所有的设备数据都是以设备为逻辑主进行更新同步的,云端无法主动且可信的修改。
  • 针对设备的主要操作(除日志、流分析为代表的非交互类逻辑应用外)都可以归类为创建/删除、配置、状态读取、功能调用这 4 类。
  • 针对同一类设备,所期望提供的功能都是一致的。

有了这几个前提,接下来就可以在充分利用这些前提的基础上,大胆的进行无阉割通用描述方式的定义了。

五元组

任何一类物联网应用终端,在上述前提下,我们都可以通过一个五元组来进行定义描述:

表 1.五元组定义

说明 ID 可以用来唯一标识一个物理终端的识别码 车牌号 固有属性 终端出厂即固定无法修改的属性 厂商/批次/型号 可配属性 可以通过某种方式调整并影响工作状态的参数 绑定账号/烘烤温度 只读属性 只能由终端上报且不可配置的属性 信号强度 功能函数 通过特定传入参数,设备所有可执行的操作 开锁/煮饭

通过清晰完整的五元组描述,针对下述场景基本就可以实现 100%的覆盖了:

  • 查询某一个终端的出厂信息及型号(固有属性)
  • 调整某一个终端的工作参数(可配属性)
  • 终端主动上报工作状态(只读属性)
  • 启动某一个终端的特定功能(功能函数)

1. 批量操作由于设备操作的设备变量差异性存在,所以完全等价于选择一批设备 的 I D ,然后逐个操作。 2. 读操作由于获取的是瞬时状态,所以是异步瞬态查询,非交互操作。

虽然看起来好像描述得很笼统,但是这确实足够涵盖绝大部分 IoT 终端的基本定义元素了。所提供的以 ID 为标识进行的设备与属性同步操作为基础的原子操作粒度,基本可以涵盖各类能想象到的物联网应用场景了。随便举几个例子:

表 2.五元组例

对象 操作 五元组等价 电表 抄电表号为 A 的表读数 功能函数{读数}({A}) -> {读数} 货车 A 火车上报位置 更新 A 的云端只读属性{位置} 智能灯 调整灯 A 的亮度 配置 A 的可配属性{亮度} 智能锁 开 A 的锁 功能函数{开锁}({A}) 智能音箱 更换 A 的人声 配置 A 的可配属性{口音}

所以既然可以利用五元组来进行所有行为的描述,那么也就可以将操作脚本化、规范化,进而支持自动化调度、设备间调度以及非定制接口下的功能开发等相关事情。其实类比来看,这种描述方式与 Java 的 interface/abstract 特性有些相像,也即约定一些一类对象该有的方法、属性,然后使用方就可以使用了,不同的终端只需要根据定义的这些框架方案做自己的实现即可。接入层与应用层遵循同一套描述语言,也大幅降低了开发需要的人力投入和时间投入。

树与 YML

设备的层级关系

有了初步的方法描述,接下来就是开始着手进行中间语言的设计了。在进行具体定义之前,我们还需要解决一个问题,也即通过单一五元组的抽象,并不能够很好的描述所有设备,或者某些设备是存在依附和组合关系的。

举个典型的例子,一个储物柜会有非常多的柜门,而每个柜门都可以认为是一个独立的小设备,可以使用一套自己的独立五元组进行描述:

表 3.柜门的五元组

类型 参考值 门号 ID 数字编号 尺寸 固有属性 - 启用 可配属性 - 状态 只读属性 开、关、异常等 开箱 功能函数 -

但是,单独通过门号是无法直接定位一个柜门的,由于柜体对柜门是典型的一对多的关系,所以通过“柜体 ID:门号 ID”就可以唯一的标定一个独立柜门。而这种需要分拆五元组的情况,实际上绝大部分都是属于从属关系,或者描述为子设备也没有问题。也就是说,通过向上遍历到跟节点的过程中,所有遇到的 ID 属性(包括自己的 ID 属性),自上而下的形成有序 ID 组,是定位一个子设备的充分必要条件。但是注意,如果构建的 ID 组中,某个 ID 缺失也不影响对子设备的唯一标定的话,意味着定义树是有问题,需要调整优化的。

注意:虽然理论上我们也可以通过{柜体 ID-门号 ID}这种拼接字串的方式构造一个独立的柜门五元组,但是这并不能有效的反应实际设备的特征。

至此,我们通过一个树形结构组织的五元组定义体系,已经可以有效且贴切的描述绝大多数的物联网终端了,接下来的工作便是寻找一种描述语言,来进行相关结构描述了。

为什么是 YML

作为能有效描述五元组和树形结构的描述语言,有几种可能的选择,如 Protobuf、Java、JSON、YML 等,其中优缺点如下表统计:

表 4.描述语言对比

语言 优势 劣势 Protobuf 有完善的多语言映射工具和语法工具 定义较为复杂,工具链的应用环境较麻烦 Java 面向对象有天然的支持且贴近应用 有很多的约定特性是多余的 JSON 简洁的数据定义模型,字典和数组的组合可以有效描述,灵活性强 为了可以字符串化,标号过多,影响直观可读性,手写容易搞错 YML 极致简洁,而且刚刚好的贴合了设计要求
是的,依稀记得第一次见到 yml 格式时的激动情绪 需要自己写解析,不过这都不是事

是的没错,我就是看上 YML 了。在实际定义前,我们再引入一个 YML 的约束规则,即针对存储对象,我们通过前缀来区分五元组的不同的属性,约定如下:

表 5.YML 约定

前缀 类型 k- ID p- 固有属性 c- 可配属性 r- 只读属性 f- 功能函数

注意:这只是个最简模型,通过利用其他的辅助描述(如结构化注释),我们可以实现更多的跟进属性带入以及对取值引入约束,事实上项目中也是这么做的。

举个例子

为了充分描述 YML 的简洁,我们引入一个比较典型的终端定义的实例来说明这种体系描述的用法。这个实例我们就抽象一个十分智能的自行车吧,给出 yml。

bike:

k-bid: # 自行车 ID

p-brand: # 品牌

p-model: # 型号

p-cap: # 载重

c-seat_height: # 座椅高度

r-speed: # 车速

f-stop: # 刹车

light: # 车灯

p-power: # 功率

c-light: # 亮度百分比

r-bat: # 电量

f-open: # 开灯

f-cloes: # 关灯

wheel: # 轮子

k-loc: # 0= 前轮, 1= 后轮

p-size: # 尺寸

r-rpm: # 转速

r-pressure: # 胎压

...

针对一个自行车整体,我们通过一个 ID 来独立标示它,这个 ID 在出厂后就唯一确定了,一般来讲可以是设备出厂写死到 OTP 的序列串号,也可以是联网网卡(是的,不能上网的自行车不叫物联网自行车...)的 MAC 地址,总之每两个自行车的号不一样就对了。针对自行车来说,品牌、型号、载重是生命周期内不可更改的,所以归类到固定属性。座椅高度既然我们都假设它"十分智能"了,自然也是液压电动可调整的了,所以我们可以通过参数来直接调整它的高度,归类到可配属性。自行车可以获取自己当前的运行速度,由于这个值只要动起来就是持续变化的,所以我们归类到只读属性里。当然为了例子完整些,我们再给他加入一个电动刹车的功能 break,归类到功能函数里。

一个自行车只有一个车灯(为了典型,假设只有一个前向车灯),所以只需要通过自行车的 ID 就可以唯一确认和控制一个车灯了。原则上来说,我们可以将车灯的属性完整并入一个自行车的五元组里,但是为了高可读性和现状拟合,我们还是将它单独划分一个子类,然后引入一个递归定义的约定:如果一个子节点没有 ID 属性定义,则它的 ID 属性完全等价于它的直属父节点 ID 属性组。这样我们就可以清晰的拆分描述主子设备了。

而一个"具备胎压检测可以自测转速的十分智能的"车轱辘,与自行车就属于一对多的映射关系了,我们引入了一个 loc 的 ID 属性,来标定是前轮还是后轮(当然,前后轮不一样是完全有可能的)。

通过这样一种描述,我们就可以完成对一辆智能自行车的抽象定义了(实际情况会更佳复杂,这只是一个演示),不管是哪个厂商生产的自行车,都可以按照这个 yml 定义的数据结构进行约定套用,而功能或者配置上的调整,只需要增扩新的 yml 字段和子节点就可以完成了。如果只允许单向增的化,系统就自动具备了向后兼容的特新了。

结束语

通过这么多字的描述,基本说清了这种数据描述方法的来龙去脉以及应用方式方法。虽然相比于这套原型式的方法论描述,实际应用时的定义要更加复杂且灵活,但是核心思想还是完全一致的。目前这套理论框架下接入的设备已经数以十万计了,围绕这套理论而构建的软件架构体系和协作体系均表现出了完全达到预期的效果,在高效解耦、全分布式部署、热插拔式的应用接入和新设备扩展、全被动式关键路径埋点等等各个方面都展现出十分诱人的特性优势,后续有时间将再参考技术实现、通用式应用等方面给出系列相关文章。

这个理论框架的构建模型和过程,也让我深深的理解到什么是聪明的工作方式。如果不做理论沉淀,跟传统方式一样接入一家供应商的一种产品就进行一套对接,然后以接口协商的流程建立同各应用系统的关系,就会不停的陷入沟通与软件质量的泥潭,进入看似每天加班忙忙碌碌但是总感觉事情做不完,好多事情是重复的状态。也建议读者在工作过程当中多思考,看是否可以进行方法论的抽象、工具化的改造,能否发现理论上的方案并付诸落地,相信会给大家打来非常大的提升。

由于本方法比较年轻,解决问题的范围也有限,仍在不断快速的迭代过程中,如果读者有相关的兴趣、问题、见解或者建议,亦或者找到本方法的缺陷,也欢迎邮件联系我,毕竟公开和大量有效的沟通交流是设计逐步完善的必经之路。

参考资源


Recommend

About Joyk


Aggregate valuable and interesting links.
Joyk means Joy of geeK