IoT设备影子功能介绍与实现
IoT设备影子功能介绍与实现
ivansliIoT平台架构
在物联网领域,应用往往会通过调用集成并实现了MQTT协议的broker的平台与设备进行数据通信,已达到端、云互动与数据流转。常见的IoT架构图如下所示:
在IoT平台中为了达到控制设备实现的功能比较多,但是大致分为3类:
- 指令控制 (云端控制设备的动作与操作)
- 属性控制 (云端更新or获取设备拥有的属性)
- 事件控制 (设备触发某些事件上报给云端)
下面我们来聊聊属性控制中常用的功能-设备影子的实现原理,并通过一些细节来研究其交互过程。
设备影子作用
设备影子数据模型是一个json格式数据,主要用于:存储设备当前上报的属性值和IoT平台期望下发给设备的属性值,且设备影子功能只存储最近一次的上报值和属性值。用户可通过应用或直接操作IoT平台API来查询或修改设备影子,以达到获取设备最新属性或将期望属性下发给设备的操作。
引用自:https://support.huaweicloud.com/intl/zh-cn/productdesc-IoT/iot_04_0006.html (下文中应用程序简称应用)
查询设备属性状态:
应用直接向设备查询状态时,由于设备可能长时间处于离线状态或因网络不稳定掉线,因此不能及时获取设备当前的状态。使用设备影子机制,设备影子保存的是设备最新的状态,一旦设备状态产生变化,设备会将状态同步到设备影子。应用便可以及时获取查询结果,无需关注设备是否在线。
很多应用频繁的查询设备状态,由于设备处理能力有限,频繁查询会损耗设备性能。使用设备影子机制,设备只需要主动同步状态给设备影子一次,多个应用程序请求设备影子获取设备状态,即可获取设备最新状态,从而将应用程序和设备解耦。修改设备属性信息:
设备管理员通过管理门户或者调用IoT平台API接口修改设备的属性信息,由于设备可能长时间处于离线状态,修改设备属性的操作不能及时下发给设备。在这种情况下,IoT平台可以将修改设备的属性信息存储在设备影子中,待设备上线后,将修改的设备属性值同步给设备,从而完成设备属性的修改。
设备影子数据粒度
业界通常将设备影子数据定义为一个设备有且仅有一个,此时意味着当前设备中不能存在同名的属性,否则无法在设备影子进行区分。
假设某些公司或特殊场景中将设备划分为若干模块,不同模块中允许定义同名属性,此时设备影子使用业界常用的定义方式将会出现问题。针对当前情况,可以允许每个设备拥有多个设备影子数据,即:设备影子数据的粒度为设备-模块
。
设备影子粒度 | 每台设备拥有的影子个数 | 是否允许存在同名属性 |
---|---|---|
设备 | 有且仅有一个 | 不允许 |
设备-模块 | 等于模块个数 | 不同模块中允许存在 |
设备影子数据格式
目前,由于没有任何国际组织对设备影子数据格式进行规范化。所以,一般参考常见云厂商影子数据格式。文档如下所示:
业界常用设备影子数据格式:
1 | { |
为了在设备影子中直观地看出是属于哪个设备的,一般优化为如下所示:
1 | { |
示例:
1 | { |
如果设备影子粒度是设备-模块
,则需要在影子数据中添加模块名称,如下所示:
1 | { |
示例:
1 | { |
设备影子版本号评估
假设每秒消耗 100万设备影子id,设备影子版本号使用 int64、uint64 类型分别可以使用的年限为:
数据类型 | 类型最大值 | 使用年限 |
---|---|---|
int64 | 9223372036854775807 | 9223372036854775807/1000000/3600/24/365 = 292471 年 |
uint64 | 18446744073709551615 | 18446744073709551615/1000000/3600/24/365 = 584942 年 |
版本号的初始值为1,影子版本号可以使用发号器实现,依次递增每次+1,不会出现重复的情况
考虑编程语言对数据类型的支持,一般使用int64类型即可。
设备影子版本号发号器实现
通过设备影子版本号评估
可以得知,使用int64作为设备影子版本号,会有一个发号器为每个设备影子生成新的可用版本号。考虑版本号是数值类型递增的,并且在一定的场景下需要满足一定的性能。可以使用Redis+DB
来通过号段的方式,使用lua脚本来生成可用设备影子版本号。
由于设备影子发号器是基于号段来生成的,所以需要号段的最大值与当前可用值。在Redis中使用hash类型来表示一个设备影子的发号器结构,有2个filed来分别表示号段的最大值(v_h)与当前最大值(v_l),lua脚本代码逻辑实现如下所示:
1 | -- KEYS[1] 发号器结构的key |
当发号器数据在Redis中不存在或者被消耗完,都需要申请一段新的号段。该号段数据的记录,则用DB设备影子数据中的字段max_version来表示。每次申请新的号段都会用到一个号段步长(step),步长数值大小可以视情况而定,一般设置为512或者1024。
发号器数据场景 | DB中max_version | Redis中新的号段空间 | 备注 |
---|---|---|---|
发号器数据不存在 | max_version+=2*step | [max_version+=step,max_version+=2*step] | 会跳过一段数值 |
发号器数据耗尽 | max_version+=step | [max_version,max_version+=step] | 数值空间连续 |
对于现在的分布式服务来说同一个服务一般都是多个进程在运行,所以在出现发号器数据在Redis中不存在或者被消耗完,申请新的号段数据时需要使用分布式锁,抢到锁的进程去进行信号段的申请。
新号段申请操作过程如下所示:
- 去Redis获取新的版本号
- 发号器数据在Redis中不存在或者被消耗完,则抢分布式锁
- 抢锁成功则double check,再次去Redis获取新的版本号。如果发号器数据在Redis中显示仍旧不存在或者被消耗完,则进入下一步。否则返回获取的版本号
- 去DB申请新的号段
- 新的号段数据写会Redis
- 去Redis获取新的版本号并返回
发号器申请新的号段在并发场景下,一般只有一个进程从步骤1到步骤6执行一遍,其他的只是从步骤1到步骤3。
设备影子版本号判断逻辑
设备影子处理过程的基本逻辑:设备影子大版本号数据 覆盖 设备影子小版本号数据,设备影子小版本号数据在操作时不予处理。 对于应用、IoT平台、设备在数据流转过程中,对设备影子版本号判断逻辑如下所示:
- | 应用 | IoT平台 | 设备 |
---|---|---|---|
应用 | - | ①应用带版本号大于IoT平台影子版本号则合并 ②应用带版本号小于等于IoT平台影子版本号则不处理 ③应用不带版本号则IoT平台自动生成并下发到设备 |
- |
IoT平台 | - | - | ①IoT平台下发设备影子版本号大于等于设备本地版本号设备则执行 ②IoT平台下发设备影子版本号小于设备本地版本号设备则不予处理 |
设备 | - | ①设备上行版本号等于IoT平台版本号则处理 ②设备上行版本号小于IoT平台版本号则不处理 ③设备上行版本号不可能大于IoT平台版本号 |
- |
设备影子数据的存储实现
设备影子数据作为一个端、云交互使用比较频繁的数据,在存储实现层面采用cache+DB
的方式。
cache使用分布式Redis集群存储,即满足高性能的数据读、写,又满足大容量的数据存储。同时,由于某些设备可能会长时间不在线或者出库之后一直没有使用,这些设备影子数据存储在cache中会导致资源的浪费,所以会对Redis中存储的设备影子数据需要设置过期时间(一般为30天+随机5天时间)。 每次读、写Redis中存储的设备影子数据都会按照一定的概率重置过期时间,这样就可以使频繁操作的数据一直存储在Redis中。
DB作为设备影子数据持久化存储的一部分,用来在Redis中数据失效被淘汰之后,从DB中重新加载到Redis中使用。考虑数据的一致性与出现问题的概率,可以认为只要写Redis成功设备影子数据就是写成功。在设备影子数据Redis写成功之后,再通过队列异步的方式落库DB。此时,假设写Redis成功落库DB失败,也会在未来的某一时刻再次有新的设备影子数据落库成功,最终达到数据的一致性。
设备影子交互&场景分析
应用->IoT平台->设备
场景 | 情况 | 影子数据可能发生变动的数据 |
---|---|---|
应用更新设备属性 | - | state.desired metadata.desired timestamp shadow_version |
应用获取设备属性(触达设备) | ①阻塞 ②非阻塞 |
- |
应用获取设备属性(仅查询IoT平台影子数据) | - | - |
设备->IoT平台->应用
场景 | 情况 | 影子数据可能发生变动的数据 |
---|---|---|
设备连接云端 | ①开机初次联网 ②断网再次联网 |
state.desired state.reported metadata.reported timestamp shadow_version |
设备本地属性变化主动上报 | - | state.reported metadata.reported timestamp shadow_version |
设备影子常见问题
问1:什么时候设备影子版本号会发生变化?
答:只要引发state.desired发生变化,版本号都+=1,其他情况版本号一般不变。问2:应用下行什么时候需要携带版本号?
答:读操作不需要版本号,写操作可以先申请版本号(也支持不带版本号的情况,此时IoT平台自动生成)。问3:IoT平台下行desired时,云/端的交互动作都有哪些?
答:state.desired非空,IoT平台下发desired,设备执行完之后上报执行结果与最新属性值。问4:应用与IoT平台交互动作有哪些?
答:获取影子版本号,读、写设备属性数据。
应用->IoT平台->设备
场景1:应用更新设备属性
步骤 | 影子数据可能发生变动的数据 | 备注 |
---|---|---|
第2步 | 发号器从设备影子号段中获取一个可用版本号 | 可选 |
第5步 | ①合并应用desired数据到设备影子state.desired ②更新设备影子metadata.desired ③更新设备影子timestamp、shadow_version |
|
第9步 | ①删除设备影子state.desired中执行成功的属性 ②更新设备影子metadata.reported ③更新设备影子timestamp、shadow_version |
场景2:应用获取设备属性
情况1:穿透到设备(阻塞)
情况2:穿透到设备(非阻塞)
场景3:应用获取设备属性(仅读取设备影子)
场景2与场景3的主要区别有2点:
① 场景2触达设备,场景3不触达设备而直接查询IoT平台设备影子数据
② 场景2-情况1与场景3返回的数据格式一样,场景2-情况2返回2次数据,场景3仅返回一次数据
为了解决场景2、场景3的数据返回格式与数据流转的不同,可以统一使用场景2-情况2的交互方式。即:都采用异步的方式进行处理。无论是否触达设备都返回2次数据,先返回唯一消息id给应用,再返回具体的设备属性信息给应用。
设备->IoT平台->应用
场景1:设备连云时,云/端同步
情况1:设备初次联网
情况2:设备断网后再次联网
情况1与情况2看似交互流程、数据流转相同,但是却存在一些不同点:
① 情况1设备端发起请求时会携带开机联网标识符,是设备端主动获取
② 情况2由MQTT broker触发告知IoT平台,IoT平台主动下发需要执行的desired
③ 情况2被触发时,若IoT平台不存在需要执行的desired,则不会有desired下发到设备
④ 第一次开机联网与断网联网时,MQTT broker都会被触发并告知IoT平台,IoT平台无法有效区分,都会执行情况2
⑤ 可能存在情况2先被执行之后,情况1再被执行的情况,但最终结果不会对设备影子数据造成任何影响
场景2:设备本地属性发生变化,主动上报
小结
目前来看,并没有任何一家公司或者组织来针对该IoT能力标准化,业界都是基于相似json格式通过数据流转来实现。对于设备影子来说,其仅仅作为IoT领域的一个小功能,实现设备属性信息在云端数据的一个映射,能够解决一些问题,提高查询效率、减少设备功耗。
通过上文可以看出其实现逻辑比较复杂,甚至有些场景还需要取舍。考虑设备影子数据不需要强一致性,所以在设计时只要实现最终一致性即可。