百事通!浅析五种物联网数据模型
2022-08-15 15:44:06来源:今日头条
在 Cassandra 中存储、查询和分析 IoT 设备生成的时间序列的最流行用例已得到很好的理解和记录。通常,时间序列是根据其源 IoT 设备存储和查询的。但是,还有另一类 IoT 应用程序需要快速访问由一组 IoT 设备基于已知状态生成的最新数据。此类应用程序需要回答的问题是:哪些物联网设备或传感器当前正在报告特定状态?在这篇博文中,我们将重点关注这个问题,并提供五种可能的数据建模解决方案,以便在 Cassandra 中有效地回答这个问题。
【资料图】
介绍物联网 (IoT) 正在生成大量需要存储、查询和分析的时间序列数据。Apache Cassandra 是这项任务的绝佳选择:不仅因为它的速度、可靠性和可扩展性,还因为它的内部数据模型内置了对时间排序数据的支持。
在 Cassandra 中,时间序列通常由源(例如,IoT 设备或传感器)或主题(例如,参数或度量)存储和检索。有很多很好的资源非常详细地涵盖了这个主题,包括这个会议演示视频,以及用于传感器数据和时间序列的即用型 Cassandra 数据模型。
在这篇文章中,我们研究了一类相关的物联网用例,它们需要管理来自许多物联网设备的最新数据的快照。此外,需要根据物联网设备报告的特定状态来查询或过滤这样的快照。换句话说,我们应该能够在 Cassandra 中快速回答这个问题:哪些物联网设备当前正在报告特定状态?对于许多现实生活中的用例,这个问题听起来更像是:
智能家居中当前打开(关闭)哪些灯?停车结构中当前有哪些停车位被占用(空置)?当前在特定位置附近有哪些车辆可用(不可用)?当前在某个区域触发(激活、禁用)哪些安全警报?建筑物中当前打开(关闭、锁定、解锁)哪些门?哪些火灾探测传感器当前报告传感器网络中的异常(正常待机、错误)状态?在下文中,我们更正式地定义了这个问题,并通过示例 CQL 实现提出了五个实用的解决方案。
问题定义给定一组 IoT 设备或传感器,这些设备或传感器生成包含时间戳、数据点和状态的按时间排序的事件序列,找到所有 IoT 设备报告的具有已知状态的最新事件。这个问题的三个关键组成部分如下图所示,描述如下:
输入由物联网设备生成的时间序列组成。时间序列通常存储在一个或多个 Cassandra 表中。中间视图是 IoT 设备报告的仅最新事件的快照。可以单独显式存储最新事件,也可以根据输入动态计算它们。最终结果是所有具有已知状态的最新事件。具有相同状态的最新事件应该存储在一起或易于计算。基于状态管理最新的 IoT 事件我们确定了基于状态管理最新物联网事件的几个挑战:
最新事件的快照不断发展。可能需要额外的努力来增量捕获任何更改。事件发生的频率通常是不可预测的。仅基于事件的时间戳组件可能难以对事件进行分区和组织。一个状态通常只能采用几个唯一值。基于低基数列对数据进行分区和索引可能会导致大分区。我们使用以下运行示例作为起点。表 events_by_device 是输入。这张具有多行分区的表旨在存储时间序列,这样每个分区对应一个设备,分区中的行表示具有时间戳、状态和值的事件。每个分区中的事件始终按其时间戳降序排序。该表实质上每个分区存储一个时间序列。我们将五个事件插入表中并检索一个设备的时间序列。此外,在第二个查询中,我们证明可以动态计算所有设备的所有最新事件。不幸的是,我们不应该依赖这个查询来解决问题:它可能会变得非常昂贵,因为它访问表中的每个分区。
架构:
CQL
-- All events by deviceCREATE TABLE events_by_device ( device_id UUID, timestamp TIMESTAMP, state TEXT, value TEXT, PRIMARY KEY((device_id), timestamp)) WITH CLUSTERING ORDER BY (timestamp DESC);
数据:
CQL
-- Event 1-1INSERT INTO events_by_device (device_id, timestamp, state, value)VALUES (11111111-aaaa-bbbb-cccc-12345678abcd, "2021-01-01 01:11:11", "on", "event 1-1");-- Event 1-2INSERT INTO events_by_device (device_id, timestamp, state, value)VALUES (11111111-aaaa-bbbb-cccc-12345678abcd, "2021-01-01 02:22:22", "off", "event 1-2");-- Event 1-3INSERT INTO events_by_device (device_id, timestamp, state, value)VALUES (11111111-aaaa-bbbb-cccc-12345678abcd, "2021-01-01 03:33:33", "on", "event 1-3");-- Event 2-1INSERT INTO events_by_device (device_id, timestamp, state, value)VALUES (22222222-aaaa-bbbb-cccc-12345678abcd, "2021-02-02 01:11:11", "off", "event 2-1");-- Event 3-1INSERT INTO events_by_device (device_id, timestamp, state, value)VALUES (33333333-aaaa-bbbb-cccc-12345678abcd, "2021-03-03 01:11:11", "off", "event 3-1");
查询:
CQL
-- Find all events for a deviceSELECT device_id, timestamp, state, valueFROM events_by_deviceWHERE device_id = 11111111-aaaa-bbbb-cccc-12345678abcd; device_id | timestamp | state | value--------------------------------------+---------------------------------+-------+----------- 11111111-aaaa-bbbb-cccc-12345678abcd | 2021-01-01 03:33:33.000000+0000 | on | event 1-3 11111111-aaaa-bbbb-cccc-12345678abcd | 2021-01-01 02:22:22.000000+0000 | off | event 1-2 11111111-aaaa-bbbb-cccc-12345678abcd | 2021-01-01 01:11:11.000000+0000 | on | event 1-1-- Find the latest events for all devicesSELECT device_id, timestamp, state, valueFROM events_by_devicePER PARTITION LIMIT 1; device_id | timestamp | state | value--------------------------------------+---------------------------------+-------+----------- 33333333-aaaa-bbbb-cccc-12345678abcd | 2021-03-03 01:11:11.000000+0000 | off | event 3-1 22222222-aaaa-bbbb-cccc-12345678abcd | 2021-02-02 01:11:11.000000+0000 | off | event 2-1 11111111-aaaa-bbbb-cccc-12345678abcd | 2021-01-01 03:33:33.000000+0000 | on | event 1-3
请注意,我们可以假设每个设备的事件数不超过 100,000。否则,我们可能不得不通过在其分区键定义中引入另一列来进一步拆分表 events_by_device 中的分区。由于这对于我们在这篇文章中解决的问题并不重要,所以让我们保持简单。
鉴于问题定义和IoT 事件的运行 CQL 示例,我们准备描述具有不同特征的五种解决方案。
解决方案一:物化视图第一个解决方案需要一个新表和一个物化视图。表 latest_events_by_device 是一个单行分区表,其中每个分区对应一个设备,每一行对应最新的已知事件。此表的目的是仅获取 IoT 设备报告的最新事件的快照。该表也是物化视图 latest_events_by_state 的基表,可以使用状态查询最新事件。
请注意,完全相同的数据被插入到表 events_by_device 和 latest_events_by_device 中。但是,对于后者,插入变为更新插入,将行更新为最新事件。
架构:
CQL
-- Latest known events by deviceCREATE TABLE latest_events_by_device ( device_id UUID, timestamp TIMESTAMP, state TEXT, value TEXT, PRIMARY KEY((device_id)));-- Latest events by stateCREATE MATERIALIZED VIEW latest_events_by_state AS SELECT * FROM latest_events_by_device WHERE state IS NOT NULL AND device_id IS NOT NULLPRIMARY KEY ((state), device_id);
数据:
CQL
-- Event 1-1INSERT INTO latest_events_by_device (device_id, timestamp, state, value)VALUES (11111111-aaaa-bbbb-cccc-12345678abcd, "2021-01-01 01:11:11", "on", "event 1-1");-- Event 1-2INSERT INTO latest_events_by_device (device_id, timestamp, state, value)VALUES (11111111-aaaa-bbbb-cccc-12345678abcd, "2021-01-01 02:22:22", "off", "event 1-2");-- Event 1-3INSERT INTO latest_events_by_device (device_id, timestamp, state, value)VALUES (11111111-aaaa-bbbb-cccc-12345678abcd, "2021-01-01 03:33:33", "on", "event 1-3");-- Event 2-1INSERT INTO latest_events_by_device (device_id, timestamp, state, value)VALUES (22222222-aaaa-bbbb-cccc-12345678abcd, "2021-02-02 01:11:11", "off", "event 2-1");-- Event 3-1INSERT INTO latest_events_by_device (device_id, timestamp, state, value)VALUES (33333333-aaaa-bbbb-cccc-12345678abcd, "2021-03-03 01:11:11", "off", "event 3-1");
查询:
CQL
-- Find all the latest events with state "on"SELECT state, device_id, timestamp, valueFROM latest_events_by_stateWHERE state = "on"; state | device_id | timestamp | value-------+--------------------------------------+---------------------------------+----------- on | 11111111-aaaa-bbbb-cccc-12345678abcd | 2021-01-01 03:33:33.000000+0000 | event 1-3-- Find all the latest events with state "off"SELECT state, device_id, timestamp, valueFROM latest_events_by_stateWHERE state = "off"; state | device_id | timestamp | value-------+--------------------------------------+---------------------------------+----------- off | 22222222-aaaa-bbbb-cccc-12345678abcd | 2021-02-02 01:11:11.000000+0000 | event 2-1 off | 33333333-aaaa-bbbb-cccc-12345678abcd | 2021-03-03 01:11:11.000000+0000 | event 3-1
物化视图解决方案具有以下特点:
适用性:基于状态的查询返回 100K 行/100MB 或更少的数据。优点:视图是自动维护的;完美的表现。缺点:物化视图有一些限制;数据分布可能会出现偏差。为了支持多租户,我们可以把表的主键改成PRIMARY KEY((tenant, device_id))或者PRIMARY KEY((tenant), device_id),物化视图的主键改成PRIMARY KEY((tenant, state), device_id )。多租户也可能有助于改善数据分布。
只要您了解并愿意抵消物化视图的限制,此数据模型就可以成为许多应用程序的简单、有效和高效的选择。这种数据模型的另一个不太明显的优势是从 Apache Pulsar 或 Apache Kafka 等事件流平台提供数据是多么容易。所有事件都可以转到基表,而物化视图负责其余的事情。
方案二:二级索引第二种解决方案需要一个新表和一个二级索引。该表与物化视图解决方案中的表相同。表 latest_events_by_device 是一个单行分区表,其中每个分区对应一个设备,每一行对应最新的已知事件。此表的目的是仅获取 IoT 设备报告的最新事件的快照。为该表创建二级索引latest_events_by_state_2i,用于根据状态查询最新事件。
再一次,完全相同的数据被插入到表 events_by_device 和 latest_events_by_device 中。但是,对于后者,插入变为更新插入,将行更新为最新事件。
架构:
CQL
-- Latest known events by deviceCREATE TABLE latest_events_by_device ( device_id UUID, timestamp TIMESTAMP, state TEXT, value TEXT, PRIMARY KEY((device_id)));-- Latest events by stateCREATE INDEX latest_events_by_state_2i ON latest_events_by_device (state);
数据:
CQL
-- Event 1-1INSERT INTO latest_events_by_device (device_id, timestamp, state, value)VALUES (11111111-aaaa-bbbb-cccc-12345678abcd, "2021-01-01 01:11:11", "on", "event 1-1");-- Event 1-2INSERT INTO latest_events_by_device (device_id, timestamp, state, value)VALUES (11111111-aaaa-bbbb-cccc-12345678abcd, "2021-01-01 02:22:22", "off", "event 1-2");-- Event 1-3INSERT INTO latest_events_by_device (device_id, timestamp, state, value)VALUES (11111111-aaaa-bbbb-cccc-12345678abcd, "2021-01-01 03:33:33", "on", "event 1-3");-- Event 2-1INSERT INTO latest_events_by_device (device_id, timestamp, state, value)VALUES (22222222-aaaa-bbbb-cccc-12345678abcd, "2021-02-02 01:11:11", "off", "event 2-1");-- Event 3-1INSERT INTO latest_events_by_device (device_id, timestamp, state, value)VALUES (33333333-aaaa-bbbb-cccc-12345678abcd, "2021-03-03 01:11:11", "off", "event 3-1");
查询:
CQL
-- Find all the latest events with state "on"SELECT state, device_id, timestamp, valueFROM latest_events_by_deviceWHERE state = "on"; state | device_id | timestamp | value-------+--------------------------------------+---------------------------------+----------- on | 11111111-aaaa-bbbb-cccc-12345678abcd | 2021-01-01 03:33:33.000000+0000 | event 1-3-- Find all the latest events with state "off"SELECT state, device_id, timestamp, valueFROM latest_events_by_deviceWHERE state = "off"; state | device_id | timestamp | value-------+--------------------------------------+---------------------------------+----------- off | 33333333-aaaa-bbbb-cccc-12345678abcd | 2021-03-03 01:11:11.000000+0000 | event 3-1 off | 22222222-aaaa-bbbb-cccc-12345678abcd | 2021-02-02 01:11:11.000000+0000 | event 2-1
二级索引方案具有以下特点:
适用性:基于状态的查询返回 100K 行/100MBs 或更多的数据;基于状态的查询很少执行。优点:在检索大型结果集时,可以更好地在集群中的节点之间分配查询工作负载。缺点:二级索引有一些限制;对于实时应用程序,性能可能会变得不令人满意。在某些情况下,这种数据模型可能是一个合理的选择。特别是,当通过将表主键更改为 PRIMARY KEY((tenant), device_id) 来引入多租户时,我们可以达到使用二级索引进行实时事务查询的最佳时机。那是在基于分区键和查询谓词中指定的索引列从大型多行分区中检索行时。
解决方案 3:状态分区表第三种解决方案依赖表 latest_events_by_state 使用状态来组织和查询最新事件。每次向该表中插入具有某种状态的事件时,都必须删除同一物联网设备的具有其他状态的任何过时事件。在我们的示例中,每个事件都有一个插入和一个删除,因为我们只有两个唯一状态。如果我们有三种可能的状态,每个新事件将导致一次插入和两次删除。
架构:
CQL
-- Latest events by stateCREATE TABLE latest_events_by_state ( state TEXT, device_id UUID, timestamp TIMESTAMP, value TEXT, PRIMARY KEY((state), device_id));
数据:
CQL
-- Event 1-1INSERT INTO latest_events_by_state (state, device_id, timestamp, value)VALUES ("on", 11111111-aaaa-bbbb-cccc-12345678abcd, "2021-01-01 01:11:11", "event 1-1");DELETE FROM latest_events_by_state WHERE state = "off" AND device_id = 11111111-aaaa-bbbb-cccc-12345678abcd;-- Event 1-2INSERT INTO latest_events_by_state (state, device_id, timestamp, value)VALUES ("off", 11111111-aaaa-bbbb-cccc-12345678abcd, "2021-01-01 02:22:22", "event 1-2");DELETE FROM latest_events_by_state WHERE state = "on" AND device_id = 11111111-aaaa-bbbb-cccc-12345678abcd;-- Event 1-3INSERT INTO latest_events_by_state (state, device_id, timestamp, value)VALUES ("on", 11111111-aaaa-bbbb-cccc-12345678abcd, "2021-01-01 03:33:33", "event 1-3");DELETE FROM latest_events_by_state WHERE state = "off" AND device_id = 11111111-aaaa-bbbb-cccc-12345678abcd;-- Event 2-1INSERT INTO latest_events_by_state (state, device_id, timestamp, value)VALUES ("off", 22222222-aaaa-bbbb-cccc-12345678abcd, "2021-02-02 01:11:11", "event 2-1");DELETE FROM latest_events_by_state WHERE state = "on" AND device_id = 22222222-aaaa-bbbb-cccc-12345678abcd;-- Event 3-1INSERT INTO latest_events_by_state (state, device_id, timestamp, value)VALUES ("off", 33333333-aaaa-bbbb-cccc-12345678abcd, "2021-03-03 01:11:11", "event 3-1");DELETE FROM latest_events_by_state WHERE state = "on" AND device_id = 33333333-aaaa-bbbb-cccc-12345678abcd;
查询:
CQL
-- Find all the latest events with state "on"SELECT state, device_id, timestamp, valueFROM latest_events_by_stateWHERE state = "on"; state | device_id | timestamp | value-------+--------------------------------------+---------------------------------+----------- on | 11111111-aaaa-bbbb-cccc-12345678abcd | 2021-01-01 03:33:33.000000+0000 | event 1-3-- Find all the latest events with state "off"SELECT state, device_id, timestamp, valueFROM latest_events_by_stateWHERE state = "off"; state | device_id | timestamp | value-------+--------------------------------------+---------------------------------+----------- off | 22222222-aaaa-bbbb-cccc-12345678abcd | 2021-02-02 01:11:11.000000+0000 | event 2-1 off | 33333333-aaaa-bbbb-cccc-12345678abcd | 2021-03-03 01:11:11.000000+0000 | event 3-1
状态分区表解决方案具有以下特点:
适用性:基于状态的查询返回 100K 行/100MB 或更少的数据。优点:出色的性能。缺点:需要额外的删除来维护表;可能需要采取措施防止与墓碑有关的问题;数据分布可能会出现偏差。在大多数情况下,这三个缺点都不应被视为严重障碍。额外的删除相当于额外的写入,Cassandra 可以轻松扩展以处理更多写入。鉴于插入和删除一次又一次地应用于相同的行,墓碑很可能在 MemTable 而不是 SSTables 中得到解决,这可以大大减少墓碑的总数。例如,对于一个给定的物联网设备,即使是频繁的状态更新都命中同一个 MemTable 也只能导致一个墓碑。我们仍然建议监控表指标以排除任何潜在问题。最后但同样重要的是,数据分布取决于数据和应用程序特征。在本文的最后一个解决方案中,我们完全控制了数据分布。
我们可以通过将表主键更改为 PRIMARY KEY((tenant, state), device_id) 轻松支持多个租户。多租户也可能有助于改善数据分布。总体而言,在性能方面,该解决方案应该可以与物化视图解决方案相媲美。
解决方案 4:多个表第四种解决方案的特点是每个州都有一个单独的表格。对表
latest_on_events_by_device 的每次插入都必须伴随着从表latest_off_events_by_device 中删除,反之亦然。这是为了确保最新事件始终取消同一设备的任何具有不同状态的过时事件。对表的基于状态的查询可能会变得非常昂贵,因为它们必须扫描表中的所有分区。
架构:
CQL
-- Latest "on" events by deviceCREATE TABLE latest_on_events_by_device ( device_id UUID, timestamp TIMESTAMP, value TEXT, PRIMARY KEY((device_id)));-- Latest "off" events by deviceCREATE TABLE latest_off_events_by_device ( device_id UUID, timestamp TIMESTAMP, value TEXT, PRIMARY KEY((device_id)));
数据:
CQL
-- Event 1-1INSERT INTO latest_on_events_by_device (device_id, timestamp, value)VALUES (11111111-aaaa-bbbb-cccc-12345678abcd, "2021-01-01 01:11:11", "event 1-1");DELETE FROM latest_off_events_by_device WHERE device_id = 11111111-aaaa-bbbb-cccc-12345678abcd;-- Event 1-2INSERT INTO latest_off_events_by_device (device_id, timestamp, value)VALUES (11111111-aaaa-bbbb-cccc-12345678abcd, "2021-01-01 02:22:22", "event 1-2");DELETE FROM latest_on_events_by_device WHERE device_id = 11111111-aaaa-bbbb-cccc-12345678abcd;-- Event 1-3INSERT INTO latest_on_events_by_device (device_id, timestamp, value)VALUES (11111111-aaaa-bbbb-cccc-12345678abcd, "2021-01-01 03:33:33", "event 1-3");DELETE FROM latest_off_events_by_device WHERE device_id = 11111111-aaaa-bbbb-cccc-12345678abcd;-- Event 2-1INSERT INTO latest_off_events_by_device (device_id, timestamp, value)VALUES (22222222-aaaa-bbbb-cccc-12345678abcd, "2021-02-02 01:11:11", "event 2-1");DELETE FROM latest_on_events_by_device WHERE device_id = 22222222-aaaa-bbbb-cccc-12345678abcd;-- Event 3-1INSERT INTO latest_off_events_by_device (device_id, timestamp, value)VALUES (33333333-aaaa-bbbb-cccc-12345678abcd, "2021-03-03 01:11:11", "event 3-1");DELETE FROM latest_on_events_by_device WHERE device_id = 33333333-aaaa-bbbb-cccc-12345678abcd;
查询:
CQL
-- Find all the latest events with state "on"SELECT device_id, timestamp, valueFROM latest_on_events_by_device; device_id | timestamp | value--------------------------------------+---------------------------------+----------- 11111111-aaaa-bbbb-cccc-12345678abcd | 2021-01-01 03:33:33.000000+0000 | event 1-3-- Find all the latest events with state "off"SELECT device_id, timestamp, valueFROM latest_off_events_by_device; device_id | timestamp | value--------------------------------------+---------------------------------+----------- 33333333-aaaa-bbbb-cccc-12345678abcd | 2021-03-03 01:11:11.000000+0000 | event 3-1 22222222-aaaa-bbbb-cccc-12345678abcd | 2021-02-02 01:11:11.000000+0000 | event 2-1
多表解决方案具有以下特点:
适用性:基于状态的查询返回 100K 行/100MBs 或更多的数据;基于状态的查询很少执行。优点:在检索大型结果集时,可以更好地在集群中的节点之间分配查询工作负载。缺点:实时应用程序的性能可能无法令人满意;需要额外的删除来维护表;可能需要采取措施防止与墓碑有关的问题。该方案在查询性能上与二级索引方案不相上下。可以通过将表主键更改为 PRIMARY KEY((tenant, device_id)) 或 PRIMARY KEY((tenant), device_id) 来支持多个租户。虽然我们在实践中不推荐这种解决方案,但这种数据模型真正有趣的是它如何为接下来讨论的可定制分区做好准备。
解决方案 5:可自定义的分区我们的最终解决方案基于为每个状态使用单独的表的想法。但是,这一次,我们使用人工桶对表进行分区。桶值很容易使用来自设备 UUID 标识符的用户定义函数散列来计算。在此示例中,该函数从 UUID 文字中提取前三位,将生成的十六进制数转换为十进制数,并返回十进制数除以 3 的余数。因此,最多可以有三个桶或每个表的分区,值为 0、1 或 2。在此示例中,我们所有的设备标识符都映射到存储桶 0 只是巧合。由于版本 4 UUID是随机生成的,因此对于大量事件,数据应该或多或少均匀分布在三个存储桶中。
与之前的数据模型类似,每次对表latest_on_events_by_bucket 的插入都必须伴随着从表latest_off_events_by_bucket 中删除,反之亦然。基于状态的查询的性能取决于分区,并且分区是可定制的。
架构:
CQL
-- Custom hash functionCREATE FUNCTION hash(id UUID) RETURNS NULL ON NULL INPUT RETURNS INT LANGUAGE Java AS "return Integer.parseInt(id.toString().substring(0,3),16) % 3;";-- Latest "on" events by deviceCREATE TABLE latest_on_events_by_bucket ( bucket INT, device_id UUID, timestamp TIMESTAMP, value TEXT, PRIMARY KEY((bucket), device_id));-- Latest "off" events by deviceCREATE TABLE latest_off_events_by_bucket ( bucket INT, device_id UUID, timestamp TIMESTAMP, value TEXT, PRIMARY KEY((bucket), device_id));
数据:
CQL
-- Event 1-1INSERT INTO latest_on_events_by_bucket (bucket, device_id, timestamp, value)VALUES (hash(11111111-aaaa-bbbb-cccc-12345678abcd), 11111111-aaaa-bbbb-cccc-12345678abcd, "2021-01-01 01:11:11", "event 1-1");DELETE FROM latest_off_events_by_bucket WHERE bucket = hash(11111111-aaaa-bbbb-cccc-12345678abcd) AND device_id = 11111111-aaaa-bbbb-cccc-12345678abcd;-- Event 1-2INSERT INTO latest_off_events_by_bucket (bucket, device_id, timestamp, value)VALUES (hash(11111111-aaaa-bbbb-cccc-12345678abcd), 11111111-aaaa-bbbb-cccc-12345678abcd, "2021-01-01 02:22:22", "event 1-2");DELETE FROM latest_on_events_by_bucket WHERE bucket = hash(11111111-aaaa-bbbb-cccc-12345678abcd) AND device_id = 11111111-aaaa-bbbb-cccc-12345678abcd;-- Event 1-3INSERT INTO latest_on_events_by_bucket (bucket, device_id, timestamp, value)VALUES (hash(11111111-aaaa-bbbb-cccc-12345678abcd), 11111111-aaaa-bbbb-cccc-12345678abcd, "2021-01-01 03:33:33", "event 1-3");DELETE FROM latest_off_events_by_bucket WHERE bucket = hash(11111111-aaaa-bbbb-cccc-12345678abcd) AND device_id = 11111111-aaaa-bbbb-cccc-12345678abcd;-- Event 2-1INSERT INTO latest_off_events_by_bucket (bucket, device_id, timestamp, value)VALUES (hash(22222222-aaaa-bbbb-cccc-12345678abcd), 22222222-aaaa-bbbb-cccc-12345678abcd, "2021-02-02 01:11:11", "event 2-1");DELETE FROM latest_on_events_by_bucket WHERE bucket = hash(22222222-aaaa-bbbb-cccc-12345678abcd) AND device_id = 22222222-aaaa-bbbb-cccc-12345678abcd;-- Event 3-1INSERT INTO latest_off_events_by_bucket (bucket, device_id, timestamp, value)VALUES (hash(33333333-aaaa-bbbb-cccc-12345678abcd), 33333333-aaaa-bbbb-cccc-12345678abcd, "2021-03-03 01:11:11", "event 3-1");DELETE FROM latest_on_events_by_bucket WHERE bucket = hash(33333333-aaaa-bbbb-cccc-12345678abcd) AND device_id = 33333333-aaaa-bbbb-cccc-12345678abcd;
查询:
CQL
-- Find all the latest events with state "on"SELECT bucket, device_id, timestamp, valueFROM latest_on_events_by_bucketWHERE bucket IN (0,1,2); bucket | device_id | timestamp | value--------+--------------------------------------+---------------------------------+----------- 0 | 11111111-aaaa-bbbb-cccc-12345678abcd | 2021-01-01 03:33:33.000000+0000 | event 1-3-- Find all the latest events with state "off"SELECT bucket, device_id, timestamp, valueFROM latest_off_events_by_bucketWHERE bucket IN (0,1,2); bucket | device_id | timestamp | value--------+--------------------------------------+---------------------------------+----------- 0 | 22222222-aaaa-bbbb-cccc-12345678abcd | 2021-02-02 01:11:11.000000+0000 | event 2-1 0 | 33333333-aaaa-bbbb-cccc-12345678abcd | 2021-03-03 01:11:11.000000+0000 | event 3-1
可定制的分区方案具有以下特点:
适用性:定制时可满足不同要求。优点:灵活性;可以通过自定义分区来优化性能。缺点:必须提供良好的分区功能;需要额外的删除来维护表;可能需要采取措施防止与墓碑有关的问题。选择一个好的分区函数是一个很好的问题。虽然这可能会增加一点复杂性,但该解决方案可以完全控制数据分区和查询性能。找到一个好的分区函数将取决于特定的数据和应用程序要求,并且可能需要一些经验和实验。例如,从 1 个分区检索 100 行通常比从 10 个分区检索 100 行快,但从 1 个分区检索 1,000,000 行通常比从 10 个分区检索 1,000,000 行慢。接下来,额外的删除相当于额外的写入,Cassandra 可以轻松扩展以处理更多写入。
鉴于插入和删除一次又一次地应用于相同的行,墓碑很可能在 MemTable 而不是 SSTables 中得到解决,这可以大大减少墓碑的总数。例如,对于一个给定的物联网设备,即使是频繁的状态更新都命中同一个 MemTable 也只能导致一个墓碑。我们仍然建议监控表指标以排除任何潜在问题。最后但同样重要的是,数据分布取决于数据和应用程序特征。在本文的最后一个解决方案中,我们完全控制了数据分布。
这种数据模型提供了极大的灵活性。通过将每个表的主键更改为 PRIMARY KEY((tenant, bucket), device_id) 可以实现多租户。更重要的是,可以更改分区函数以增加或减少分区的数量。检索较小结果集的查询应访问较少数量的分区以获得更好的性能。检索更大结果集的查询应访问更多分区以更好地分配工作负载。可以针对不同的状态和租户使用不同的功能以实现最佳性能。更好的分区应该会带来更好的性能。
结论我们定义了基于状态管理最新物联网事件的问题,确定了它的挑战,并描述了如何在 Apache Cassandra 中使用五种不同的数据模型来解决它。我们陈述了每个数据模型的适用性、优缺点。我们的最终建议是关注物化视图、状态分区表和可自定义的分区数据模型。选择前两个是因为它们简单易用。当其他选项用尽时,考虑可定制的分区以获得最大的灵活性。最后,开放探索新的可能解决方案,这些解决方案可能会将一些计算推向应用程序或依赖专门的搜索索引和其他技术。