40

数据驱动型的 IoT 体系架构设计,第 2 部分: 全异步的通用高性能物联网架构参考实践

 5 years ago
source link: http://www.ibm.com/developerworks/cn/iot/library/iot-lo-architecture-design-from-application-layer2/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 体系架构设计,第 2 部分

全异步的通用高性能物联网架构参考实践

参考实现篇

宋 辰

2019 年 4 月 24 日发布

系列内容:

此内容是该系列 3 部分中的第 # 部分: 数据驱动型的 IoT 体系架构设计,第 2 部分

https://www.ibm.com/developerworks/cn/views/global/libraryview.jsp?contentarea_by=Internet+of+Things&search_by=%E6%95%B0%E6%8D%AE%E9%A9%B1%E5%8A%A8%E5%9E%8B%E7%9A%84+IoT+%E4%BD%93%E7%B3%BB%E6%9E%B6%E6%9E%84%E8%AE%BE%E8%AE%A1&topic_by=%E6%89%80%E6%9C%89+Internet+of+Things+%E4%B8%BB%E9%A2%98&type_by=%E6%89%80%E6%9C%89+Internet+of+Things+%E7%A7%8D%E7%B1%BB%E5%9E%8B&ibm-search=%E6%90%9C%E7%B4%A2

敬请期待该系列的后续内容。

此内容是该系列的一部分: 数据驱动型的 IoT 体系架构设计,第 2 部分

敬请期待该系列的后续内容。

本文旨在给出一个参考的全异步物联网架构模型,可以覆盖基本上绝大部分的物联网应用,在前序文章所描述的数据定义规则支持下,本技术架构的设计思路理论上可以完成对如下特性和场景很好的覆盖:

  • 海量设备接入下的稳定服务能力支持
  • 不同类设备之间服务的故障阻断
  • 对水平动态无感知扩展的天然支持
  • 对各类不同接入协议设备的统一服务暴露
  • 应用与硬件接入侧完全解耦的开发模式支持
  • 插件式动态接入的应用开发扩展
  • 全被动的操作和更新行为全量埋点支持
  • 全路径的无状态服务节点

本文给出的为一个设计参考,但是更建议从设计方法论的角度来观察本文的构成内容。一个较好的架构设计实现必须依赖于一个成熟且完备的理论支持模型 —— 能够从抽象层面上完整的覆盖与解释所有可能提供的功能,完整且无遗漏的暴露所有的功能集合,提供易用且满足需求的 服务。如果没想清楚而单纯为了应用一种思想或一种技术而去使用,往往在研发和迭代过程中会 带来层出不穷甚至硬伤式的问题,进而让一个干净的系统变得愈发繁杂,不断增加的复杂度又会进一步影响到系统的稳定性与性能,使维护和落地成本进一步攀升,这都是应当尽力避免的。另外在实践方面,本文描述的方法思路能做到每日亿级数据,近百万数量级的各类设备接入验证且持续表现稳定,各类优势特性也在各种场景下得到了广泛且有效的应用。

IoT 设备技术的多样性

异构的设备

在设计架构方案之前,还是需要先针对 IoT 系统所面临的实际现状进行一些整理。首先关于设备本身就有很大的问题存在,设备的 CPU 从最低成本的、连操作系统都跑不起来的 8 位超低功耗的 MCU,到能够运行简单 RTOS 系统的 Cortex- M 级别 8 /32 位嵌入式 ARM 处理器,再到可以运行完整 linux 内核的 Cortex-A 级别 ARM/MIPS/ATOM 处理器五花八门的什么都有。联网方式上从通过 2/3/4G、NB-IoT、LoRa 这些时断时续的移动运营商网络上网的到使用 wifi 局域环境稳定上网的都有。

这些不同的 CPU 能力和差异巨大的网络环境,直接导致了设备能够处理的数据量、相应处理的延迟时效、重试机制、通信失败与异常的情况比例都存在巨大差异。有时候可能一个命令的下发,回应是分钟甚至是小时级别,所以架构在针对硬件接入这一层上,把这些因素都抽象归总在一起做到完全向上隔离,是一个非常重要的基础要求。

非标的接入协议

由于各类 IoT 设备的接入能力、厂商能力和经验以及对应通信模块固件的功能差异,在一个众厂商、多类设备的应用场景下,往往会并存各类完全不同的接入协议而没有一个公认的参考标准。一般常用的协议包括 MQTT、HTTP、自定义的 TCP 数据流、RPC 等等。针对基于这些协议之上实现的应用层规范,每个产品、每个厂商又会有自己习惯性的约束。对于时间紧张的项目,往往难以有效的与每一个供应商沟通,定义并磨合完全符合 自己定位的设备程序,毕竟对供应商来说,单独为很多厂商定制专属固件,后续在系统需要进行固件升级来解决设备 bug、新增功能等情况时,就会带来成倍增加的研发与测试成本,以及对应增加的故障发生风险和故障修复时间。那么在架构设计的时候,做到硬件接入协议部分的完整隔离就变得非常重要。

多版本并存的挑战

即使对于同一个供应商给出的同一款设备,换代升级的时候也还是会面临到一些功能新增、前向不能兼容等等问题,而且某些设备也并不天然具备 OTA 升级等动态更新固件的功能。也就是说,在实际应用场景当中,可能会存在同时有多个版本的协议并存,在这种情况下,如何能够做到全向兼容,就是一个非常重要的问题了。

基础思想

异步与同步

首先要确认的一个问题就是,相比与异步模式,同步操作是一个可能的选择么?同步操作本质上是系统线程并发机制下的实时应用模式,相比与异步模式来说,最大的好处是可以在系统极限的范围内最快速的返回期望的结果,从而获取最佳的响应时间。但是问题也恰恰出在这里,为了实现这个效果,同步过程需要一直高频率的抢占操作系统有限的调度时间片并且需要保存有完整的线程上下文开销,所以对应需要付出的代价就是过高的系统资源浪费和有限的并发能力。

从上述描述中就可以初步看出,同步的方案并不能非常好的服务于海量设备实时接入的应用场景,一则过多的设备服务会占用过多的系统资源,二则是设备由于自身性能和网络环境的影响,已经不可能提供高实时性的服务了,所以同步的优势也就不再存在。异步的实现思想基本都是基于事件驱动模型,相比与重量级的线程级同步操作,事件驱动模型可以在一个还不错的响应时间前提下,大幅度的降低对系统资源的依赖和占用,尤其是针对海量不活跃且反应慢的 IoT 设备,是优于同步模式的选择。

数据库快照及影子机制

由于设备都是由当前的状态信息与可配置的参数信息构成,且设备的状态信息往往是通过某种策略设备主动或被动反馈的,再考虑到设备反馈的时效性,理论上我们可以将设备的状态抽象成一个不定时更新的数据库表。换句话说,我们对设备的状态获取,理论上等同与对一张数据库表格的读取操作,这张表格的写入则都通过设备的上报来完成。注意,这里有个 tip,即如果我们通过下发立即同步类指令,是可以直接触发数据库更新的。这样的一个数据库表的抽象,基本就可以实现完整的业务应用解耦了,而且在不影响使用效果的前提下,也能极大的疏解性能压力,提高效率。

而针对设备可配置参数,我们将引入另一个设计方案即影子同步机制。影子与本体,分别代表着我们最后一次对设备的期望配置参数与设备反馈来最新的设备上配置参数。之所以要这样做,是因为由于网络、设备异常等各种问题的存在,一个配置下发的命令并不一定能够很好的执行成功。而由于超高延迟和重试机制的存在,配置设备的应用测也不可能做到等待每一个配置成功还是配置失败。在这种前提下,发现需要重配的设备,获取设备的要求配置参数以及获 取设备的当前工作状态就变的很重要了。影子机制的实现,在数据库里体现为两个基本完全一致定义的表。其中,所有请求设置设备参数的调用都会直接更新影子表,而所有来自设备的实际参数上报则会直接更新本体表,也就是使用者通过对本体表的查询就能获取实际的设备状态。当设备返回配置失败的时候,可以通过比较影子与本体表的差别,重新设置为最新的参数。如果请求设置设备的时候设备是离线的,那么当设备上线会主动比对一下影子与本体表是否有差异,如果有差异则立即执行配置命令。

分层架构设计方案

目标

在同步分层方案之前,我们需要先确认一下我们分层有关的架构设计目标。首先要能做到应用开发与设备接入的完全解耦,然后需要做到针对同类设备下不同厂商不同批次产品的无感知接入,最后需要被动记录所有设备的关键操作。在这个前提的约束下,我们可以把系统简单划分为三层(实际证明也是非常有效的),设备侧适配层、中间路由层、应用侧服务层。

路由层

中间这层命名为路由层,即所有的请求和通报都会经过本层来完成转发。路由层提供抽象的数据标准实现,即针对一个设备五元组(ID、固有属性、可配属性、只读属性、功能函数)的 CUDF(Create、Update、Delete、Function) 操作。但有如下几点需要注意:

  • 来自服务层的更新请求只能更新可配属性。
  • 针对可配属性更新调用配套影子机制,服务层的执行结果为更新影子配置,适配层的执行结果为更新本体配置。
  • 创建一个设备的时候 ID 和固有属性都是必带初始化参数,固有属性(除创建外)是完全只读,不提供任何修改接口支持。
  • F 操作对象只能是 f 属性,并且需要携带 f 属性约定的 arg 字典作为参数(python 的函数传参底层实现也是一个字典而不是像 C 一样的约定栈序)。
  • 除了 F 操作外,CUD 操作都会直接完成对应数据库的操作,即数据库里的数据永远等同于最新一次操作后的状态。
  • 由于设备的操作粒度是基于个体的,所以任何一次 CUDF 请求,都能且仅能携带一个设备 ID。这个很好理解,即使携带了一组 ID 做同样操作,每一个设备都可能因为网络、设备、工作状态等因素而返回不可预知的不同结果。

所以路由层的任何一次 CUDF 操作只要携带着所有的操作信息传递到对侧层,就可以完成所有指令的下发。一次 CUDF 操作在完成数据库更新的同时或之后,会在对侧消息总线上发送一条包含本次请求四元组(ID、类型 C/U/D/F、参数组 -dict、taskID)的消息。关于 taskID 对应一次请求行为结果的追踪,后续在说到服务层的时候会细说。

相比于一般数据库的 CURD 操作,本处缺少了读取功能声明,主要原因为本架构模式下,所有信息的获取都可以通过两种方式直接完成,一种是应用侧的请求发起调用后,等待来自于适配层的操作消息返回,从结果中获取最新值。另一种获取方式为直接查表获取设备的最近同步状态。拆分来看,针对固有属性肯定查表获取的值都是正确的,配置类属性则每次变更一定是应用侧发起,所以通过返回结果获取是最准确的。而设备的状态则一般为适配层自带的逻辑实现,或计划任务式的心跳判断,或设备主动上报的状态同步事件,或其他自定义的判断逻辑,这些更新都是即时写入到数据库的,而且更新频率一般情况下都是分钟级甚至更久,所以查库获取到的参数时效性正常情况都是没有问题的。

路由层是完全无状态的工作模式,而且 Kafka 类消息总线对每一个实例都是对等连接的,那么路由层就可以有任意多的节点,单次的请求都可以随机的分配到任何一个实例上执行,而且执行结果的返回也可以再次随机分配到任何一个实例节点上,这也就构成了分布式性能提升和容错设计的理论基础。

适配层

设备这一侧命名为适配层,即将所有设备的行为交互拟合转译为中心路由层的标准约束行为。设备的功能调用,可以认为是一次服务侧发起的 CUDF 操作的执行,设备的上报的状态即配置信息变更可以认为是一次服务侧请求的回应或者一次主动的设备状态更新。

值得注意的是,在分布式的多节点部署模式下,针对消息的解读有两种模式,一种是抢占式,一种为匹配式,分别对应于 Kafka 消息订阅的同 group 和异 group。如果是 mqtt 类订阅消息式的无连接应用,则发送给一个设备的消息可以由一个分布式集群中的任何一个实例发起,这种情况下抢占式的消息接收模式就很合适了,可以保证没有多余的系统资源浪费,同时又能够实现配置要求。如果是 TCP 长链接类的设备唯一绑定一个节点的应用,则需要使用匹配模式,即分布式集群中的所有节点都需要收到消息的一个拷贝副本,然后判断收到的消息是否在自己的连接池中,如果在的话则处理消息与设备交互产生结果,不在的话则直接丢弃。相比于抢占模式,匹配模式最大的缺点就在于存在大量的冗余消费行为,当节点数过多的时候这个单机匹配的命中率就会越低,所以在设备有频繁的服务侧交互且适配层分布式节点数量过多的时候,还是需要进行必要的定向优化,以提升系统的吞吐量。

适配层除了能够完全隔离来自于设备协议和性能等客观差异情况带来的隔离传播外,还可以很好的完成故障阻断和动态扩容。当某一节点甚至整个适配层集群出现故障,其他的节点或者其他的设备由于与中心路由层的依赖关系只有消息读取和请求调用,所以即使共用一个路由也不会收到负面影响,从服务层来看只是受影响的个别设备或同类设备出现的无法在有效时间内响应异常,并不会严格意义上影响系统工作状态。

另外需要注意的一个问题是,有时候我们可能会有非状态类的应用模式,如数据流的原始数据大数据存储等,这种情况下我们可以再应用层上开辟一个临时专用通道来完成对应的结果需求输出,从而可以至少临时性的满足业务侧需求。

服务层

应用解决方案这一层我们命名为服务层,路由接入方式也是包括 RPC 类 CUDF 调用请求以及监听 Kafka 总线来获取反馈消息。由于我们的数据声明模型提供了可供自由组合的完整原子操作集,所以业务侧可以根据数据模型定义按需组合出自己期望的调用模式。另外 RPC 与 Kafka 的接入友好特性,也决定了系统可以在完全不停机的前提下,支持动态的新业务应用添加并且做到对其他业务应用的完全无干扰。

当一次服务侧的请求调用,可能因为设备离线、故障、网络不稳定、性能阻塞等等各种原因面临随机性的失败、高延迟等情况,所以使用异步调用模式,即请求发出后即生成一个 taskID 追踪号返回给服务侧,同时把这个 taskID 协同本次请求发射到适配层,这样本次操作的执行过程就会全程携带 taskID,直到适配层执行完毕后,将带有 taskID 的 CUDF 操作再一次通过路由层发送到服务层的消息总线上,侦听总线的服务侧对应程序就可以通过 taskID 来甄别是否是自己发起的调用了。

全被动埋点

提到埋点,一般都是属于主动操作,通过引入各种代码来扩充监控的信息项,从而来完成对用户尽可能多类别下每一次行为的追踪。而主动编码往往面临着工作量大、采集点分散、维护升级困难等等问题,都足够让人头疼。典型的埋点数据包括请求是否成功、操作行为摘要、系统相应时间等。而拟合到本架构模型来看的话,我们可以将其进一步扩充为记录所有 CUDF 操作的结果、摘要及相应时间,这个定义可以涵盖所有的用户操作行为,并且还可以进一步涵盖所有的设备请求变更信息。

分层设计中的路由层,由于转发着所有的服务层请求以及适配层回应,以及所有的适配层通报,所以理论上只要我们将所有的请求追踪落库,写入操作 ID、Action 与携带信息,并且通过 taskID 来匹配请求与响应,我们就可以在极少的开发量支持下完成接入本架构所有智能硬件设备的所有操作与通报行为埋点了,而且即使后续新增接入设备,我们也不需要扩充我们的代码,被动就具备了这样一个新技能,是不是想想就很激动?而且还有一点,由于理论定义的完备性,我们设置不需要考虑会不会有没有覆盖到的行为操作,因为从设计本源上就决定了这套方案能记录我们能记录的最全埋点信息,简直就是广大程序员甚至产品经理们的福音!

结束语

一个好的架构设计,除了能够大幅度提高系统的性能、扩展性外,还可以极大程度上降低系统的维护成本、沟通协作的时间与人力成本,在新增应用与场景的时候,发挥越来越强大的作用。有时候架构甚至会从逻辑上催生出可能的业务方向创新思路,去进一步拓宽系统的价值面,促成一个正向反馈链条。而我想再次强调的是,通过理论角度来完成一个架构的设计,并且达到理论上最优的完备实现,再根据理论诉求来确认技术选型,真的应该是作为高级架构师的基础能力。

参考资源


About Joyk


Aggregate valuable and interesting links.
Joyk means Joy of geeK