Apache Cassandra是大規模管理 IoT 和時間序列數據的可靠選擇。在 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 中使用五種不同的數據模型來解決它。我們陳述了每個數據模型的適用性、優缺點。我們的最終建議是關注物化視圖、狀態分區表和可自定義的分區數據模型。選擇前兩個是因為它們簡單易用。當其他選項用盡時,考慮可定制的分區以獲得最大的靈活性。最后,開放探索新的可能解決方案,這些解決方案可能會將一些計算推向應用程序或依賴專門的搜索索引和其他技術。
值得一提的是,這篇博文的動機是來自 Discord 上 Apache Cassandra 社區成員的問題。立即加入(Cassandra)戒指團契,與社區成員和專家建立聯系!