Apache Cassandra是大規(guī)模管理 IoT 和時間序列數(shù)據(jù)的可靠選擇。在 Cassandra 中存儲、查詢和分析 IoT 設(shè)備生成的時間序列的最流行用例已得到很好的理解和記錄。通常,時間序列是根據(jù)其源 IoT 設(shè)備存儲和查詢的。但是,還有另一類 IoT? 應(yīng)用程序需要快速訪問由一組 IoT 設(shè)備基于已知狀態(tài)生成的最新數(shù)據(jù)。此類應(yīng)用程序需要回答的問題是:哪些物聯(lián)網(wǎng)設(shè)備或傳感器當(dāng)前正在報告特定狀態(tài)?在這篇博文中,我們將重點(diǎn)關(guān)注這個問題,并提供五種可能的數(shù)據(jù)建模解決方案,以便在 Cassandra 中有效地回答這個問題。
介紹
物聯(lián)網(wǎng) (IoT) 正在生成大量需要存儲、查詢和分析的時間序列數(shù)據(jù)。Apache Cassandra 是這項(xiàng)任務(wù)的絕佳選擇:不僅因?yàn)樗乃俣取⒖煽啃院涂蓴U(kuò)展性,還因?yàn)樗膬?nèi)部數(shù)據(jù)模型內(nèi)置了對時間排序數(shù)據(jù)的支持。
在 Cassandra 中,時間序列通常由源(例如,IoT 設(shè)備或傳感器)或主題(例如,參數(shù)或度量)存儲和檢索。有很多很好的資源非常詳細(xì)地涵蓋了這個主題,包括這個會議演示視頻,以及用于傳感器數(shù)據(jù)和時間序列的即用型 Cassandra 數(shù)據(jù)模型。
在這篇文章中,我們研究了一類相關(guān)的物聯(lián)網(wǎng)用例,它們需要管理來自許多物聯(lián)網(wǎng)設(shè)備的最新數(shù)據(jù)的快照。此外,需要根據(jù)物聯(lián)網(wǎng)設(shè)備報告的特定狀態(tài)來查詢或過濾這樣的快照。換句話說,我們應(yīng)該能夠在 Cassandra 中快速回答這個問題:哪些物聯(lián)網(wǎng)設(shè)備當(dāng)前正在報告特定狀態(tài)?對于許多現(xiàn)實(shí)生活中的用例,這個問題聽起來更像是:
- 智能家居中當(dāng)前打開(關(guān)閉)哪些燈?
- 停車結(jié)構(gòu)中當(dāng)前有哪些停車位被占用(空置)?
- 當(dāng)前在特定位置附近有哪些車輛可用(不可用)?
- 當(dāng)前在某個區(qū)域觸發(fā)(激活、禁用)哪些安全警報?
- 建筑物中當(dāng)前打開(關(guān)閉、鎖定、解鎖)哪些門?
- 哪些火災(zāi)探測傳感器當(dāng)前報告?zhèn)鞲衅骶W(wǎng)絡(luò)中的異常(正常待機(jī)、錯誤)狀態(tài)?
在下文中,我們更正式地定義了這個問題,并通過示例 CQL 實(shí)現(xiàn)提出了五個實(shí)用的解決方案。
問題定義
給定一組 IoT 設(shè)備或傳感器,這些設(shè)備或傳感器生成包含時間戳、數(shù)據(jù)點(diǎn)和狀態(tài)的按時間排序的事件序列,找到所有 IoT 設(shè)備報告的具有已知狀態(tài)的最新事件。這個問題的三個關(guān)鍵組成部分如下圖所示,描述如下:
- 輸入由物聯(lián)網(wǎng)設(shè)備生成的時間序列組成。時間序列通常存儲在一個或多個 Cassandra 表中。
- 中間視圖是 IoT 設(shè)備報告的僅最新事件的快照。可以單獨(dú)顯式存儲最新事件,也可以根據(jù)輸入動態(tài)計(jì)算它們。
- 最終結(jié)果是所有具有已知狀態(tài)的最新事件。具有相同狀態(tài)的最新事件應(yīng)該存儲在一起或易于計(jì)算。
基于狀態(tài)管理最新的 IoT 事件
我們確定了基于狀態(tài)管理最新物聯(lián)網(wǎng)事件的幾個挑戰(zhàn):
- 最新事件的快照不斷發(fā)展。可能需要額外的努力來增量捕獲任何更改。
- 事件發(fā)生的頻率通常是不可預(yù)測的。僅基于事件的時間戳組件可能難以對事件進(jìn)行分區(qū)和組織。
- 一個狀態(tài)通常只能采用幾個唯一值。基于低基數(shù)列對數(shù)據(jù)進(jìn)行分區(qū)和索引可能會導(dǎo)致大分區(qū)。
我們使用以下運(yùn)行示例作為起點(diǎn)。表 events_by_device 是輸入。這張具有多行分區(qū)的表旨在存儲時間序列,這樣每個分區(qū)對應(yīng)一個設(shè)備,分區(qū)中的行表示具有時間戳、狀態(tài)和值的事件。每個分區(qū)中的事件始終按其時間戳降序排序。該表實(shí)質(zhì)上每個分區(qū)存儲一個時間序列。我們將五個事件插入表中并檢索一個設(shè)備的時間序列。此外,在第二個查詢中,我們證明可以動態(tài)計(jì)算所有設(shè)備的所有最新事件。不幸的是,我們不應(yīng)該依賴這個查詢來解決問題:它可能會變得非常昂貴,因?yàn)樗L問表中的每個分區(qū)。
架構(gòu):
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);
數(shù)據(jù):
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
請注意,我們可以假設(shè)每個設(shè)備的事件數(shù)不超過 100,000。否則,我們可能不得不通過在其分區(qū)鍵定義中引入另一列來進(jìn)一步拆分表 events_by_device 中的分區(qū)。由于這對于我們在這篇文章中解決的問題并不重要,所以讓我們保持簡單。
鑒于問題定義和IoT 事件的運(yùn)行 CQL 示例,我們準(zhǔn)備描述具有不同特征的五種解決方案。
解決方案一:物化視圖
第一個解決方案需要一個新表和一個物化視圖。表 latest_events_by_device 是一個單行分區(qū)表,其中每個分區(qū)對應(yīng)一個設(shè)備,每一行對應(yīng)最新的已知事件。此表的目的是僅獲取 IoT 設(shè)備報告的最新事件的快照。該表也是物化視圖 latest_events_by_state 的基表,可以使用狀態(tài)查詢最新事件。
請注意,完全相同的數(shù)據(jù)被插入到表 events_by_device 和 latest_events_by_device 中。但是,對于后者,插入變?yōu)楦虏迦耄瑢⑿懈聻樽钚率录?/p>
架構(gòu):
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);
數(shù)據(jù):
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
物化視圖解決方案具有以下特點(diǎn):
- 適用性:基于狀態(tài)的查詢返回 100K 行/100MB 或更少的數(shù)據(jù)。
- 優(yōu)點(diǎn):視圖是自動維護(hù)的;完美的表現(xiàn)。
- 缺點(diǎn):物化視圖有一些限制;數(shù)據(jù)分布可能會出現(xiàn)偏差。
為了支持多租戶,我們可以把表的主鍵改成PRIMARY KEY((tenant, device_id))或者PRIMARY KEY((tenant), device_id),物化視圖的主鍵改成PRIMARY KEY((tenant, state), device_id )。多租戶也可能有助于改善數(shù)據(jù)分布。
只要您了解并愿意抵消物化視圖的限制,此數(shù)據(jù)模型就可以成為許多應(yīng)用程序的簡單、有效和高效的選擇。這種數(shù)據(jù)模型的另一個不太明顯的優(yōu)勢是從 Apache Pulsar 或 Apache Kafka 等事件流平臺提供數(shù)據(jù)是多么容易。所有事件都可以轉(zhuǎn)到基表,而物化視圖負(fù)責(zé)其余的事情。
方案二:二級索引
第二種解決方案需要一個新表和一個二級索引。該表與物化視圖解決方案中的表相同。表 latest_events_by_device 是一個單行分區(qū)表,其中每個分區(qū)對應(yīng)一個設(shè)備,每一行對應(yīng)最新的已知事件。此表的目的是僅獲取 IoT 設(shè)備報告的最新事件的快照。為該表創(chuàng)建二級索引latest_events_by_state_2i,用于根據(jù)狀態(tài)查詢最新事件。
再一次,完全相同的數(shù)據(jù)被插入到表 events_by_device 和 latest_events_by_device 中。但是,對于后者,插入變?yōu)楦虏迦耄瑢⑿懈聻樽钚率录?/p>
架構(gòu):
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);
數(shù)據(jù):
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
二級索引方案具有以下特點(diǎn):
- 適用性:基于狀態(tài)的查詢返回 100K 行/100MBs 或更多的數(shù)據(jù);基于狀態(tài)的查詢很少執(zhí)行。
- 優(yōu)點(diǎn):在檢索大型結(jié)果集時,可以更好地在集群中的節(jié)點(diǎn)之間分配查詢工作負(fù)載。
- 缺點(diǎn):二級索引有一些限制;對于實(shí)時應(yīng)用程序,性能可能會變得不令人滿意。
在某些情況下,這種數(shù)據(jù)模型可能是一個合理的選擇。特別是,當(dāng)通過將表主鍵更改為 PRIMARY KEY((tenant), device_id) 來引入多租戶時,我們可以達(dá)到使用二級索引進(jìn)行實(shí)時事務(wù)查詢的最佳時機(jī)。那是在基于分區(qū)鍵和查詢謂詞中指定的索引列從大型多行分區(qū)中檢索行時。
解決方案 3:狀態(tài)分區(qū)表
第三種解決方案依賴表 latest_events_by_state 使用狀態(tài)來組織和查詢最新事件。每次向該表中插入具有某種狀態(tài)的事件時,都必須刪除同一物聯(lián)網(wǎng)設(shè)備的具有其他狀態(tài)的任何過時事件。在我們的示例中,每個事件都有一個插入和一個刪除,因?yàn)槲覀冎挥袃蓚€唯一狀態(tài)。如果我們有三種可能的狀態(tài),每個新事件將導(dǎo)致一次插入和兩次刪除。
架構(gòu):
CQL
-- Latest events by stateCREATE TABLE latest_events_by_state ( state TEXT, device_id UUID, timestamp TIMESTAMP, value TEXT, PRIMARY KEY((state), device_id));
數(shù)據(jù):
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
狀態(tài)分區(qū)表解決方案具有以下特點(diǎn):
- 適用性:基于狀態(tài)的查詢返回 100K 行/100MB 或更少的數(shù)據(jù)。
- 優(yōu)點(diǎn):出色的性能。
- 缺點(diǎn):需要額外的刪除來維護(hù)表;可能需要采取措施防止與墓碑有關(guān)的問題;數(shù)據(jù)分布可能會出現(xiàn)偏差。
在大多數(shù)情況下,這三個缺點(diǎn)都不應(yīng)被視為嚴(yán)重障礙。額外的刪除相當(dāng)于額外的寫入,Cassandra 可以輕松擴(kuò)展以處理更多寫入。鑒于插入和刪除一次又一次地應(yīng)用于相同的行,墓碑很可能在 MemTable 而不是 SSTables 中得到解決,這可以大大減少墓碑的總數(shù)。例如,對于一個給定的物聯(lián)網(wǎng)設(shè)備,即使是頻繁的狀態(tài)更新都命中同一個 MemTable 也只能導(dǎo)致一個墓碑。我們?nèi)匀唤ㄗh監(jiān)控表指標(biāo)以排除任何潛在問題。最后但同樣重要的是,數(shù)據(jù)分布取決于數(shù)據(jù)和應(yīng)用程序特征。在本文的最后一個解決方案中,我們完全控制了數(shù)據(jù)分布。
我們可以通過將表主鍵更改為 PRIMARY KEY((tenant, state), device_id) 輕松支持多個租戶。多租戶也可能有助于改善數(shù)據(jù)分布。總體而言,在性能方面,該解決方案應(yīng)該可以與物化視圖解決方案相媲美。
解決方案 4:多個表
第四種解決方案的特點(diǎn)是每個州都有一個單獨(dú)的表格。對表
latest_on_events_by_device 的每次插入都必須伴隨著從表 latest_off_events_by_device 中刪除,反之亦然。這是為了確保最新事件始終取消同一設(shè)備的任何具有不同狀態(tài)的過時事件。對表的基于狀態(tài)的查詢可能會變得非常昂貴,因?yàn)樗鼈儽仨殥呙璞碇械乃蟹謪^(qū)。
架構(gòu):
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)));
數(shù)據(jù):
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
多表解決方案具有以下特點(diǎn):
- 適用性:基于狀態(tài)的查詢返回 100K 行/100MBs 或更多的數(shù)據(jù);基于狀態(tài)的查詢很少執(zhí)行。
- 優(yōu)點(diǎn):在檢索大型結(jié)果集時,可以更好地在集群中的節(jié)點(diǎn)之間分配查詢工作負(fù)載。
- 缺點(diǎn):實(shí)時應(yīng)用程序的性能可能無法令人滿意;需要額外的刪除來維護(hù)表;可能需要采取措施防止與墓碑有關(guān)的問題。
該方案在查詢性能上與二級索引方案不相上下。可以通過將表主鍵更改為 PRIMARY KEY((tenant, device_id)) 或 PRIMARY KEY((tenant), device_id) 來支持多個租戶。雖然我們在實(shí)踐中不推薦這種解決方案,但這種數(shù)據(jù)模型真正有趣的是它如何為接下來討論的可定制分區(qū)做好準(zhǔn)備。
解決方案 5:可自定義的分區(qū)
我們的最終解決方案基于為每個狀態(tài)使用單獨(dú)的表的想法。但是,這一次,我們使用人工桶對表進(jìn)行分區(qū)。桶值很容易使用來自設(shè)備 UUID 標(biāo)識符的用戶定義函數(shù)散列來計(jì)算。在此示例中,該函數(shù)從 UUID 文字中提取前三位,將生成的十六進(jìn)制數(shù)轉(zhuǎn)換為十進(jìn)制數(shù),并返回十進(jìn)制數(shù)除以 3 的余數(shù)。因此,最多可以有三個桶或每個表的分區(qū),值為 0、1 或 2。在此示例中,我們所有的設(shè)備標(biāo)識符都映射到存儲桶 0 只是巧合。由于版本 4 UUID是隨機(jī)生成的,因此對于大量事件,數(shù)據(jù)應(yīng)該或多或少均勻分布在三個存儲桶中。
與之前的數(shù)據(jù)模型類似,每次對表
latest_on_events_by_bucket 的插入都必須伴隨著從表 latest_off_events_by_bucket 中刪除,反之亦然。基于狀態(tài)的查詢的性能取決于分區(qū),并且分區(qū)是可定制的。
架構(gòu):
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));
數(shù)據(jù):
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
可定制的分區(qū)方案具有以下特點(diǎn):
- 適用性:定制時可滿足不同要求。
- 優(yōu)點(diǎn):靈活性;可以通過自定義分區(qū)來優(yōu)化性能。
- 缺點(diǎn):必須提供良好的分區(qū)功能;需要額外的刪除來維護(hù)表;可能需要采取措施防止與墓碑有關(guān)的問題。
選擇一個好的分區(qū)函數(shù)是一個很好的問題。雖然這可能會增加一點(diǎn)復(fù)雜性,但該解決方案可以完全控制數(shù)據(jù)分區(qū)和查詢性能。找到一個好的分區(qū)函數(shù)將取決于特定的數(shù)據(jù)和應(yīng)用程序要求,并且可能需要一些經(jīng)驗(yàn)和實(shí)驗(yàn)。例如,從 1 個分區(qū)檢索 100 行通常比從 10 個分區(qū)檢索 100 行快,但從 1 個分區(qū)檢索 1,000,000 行通常比從 10 個分區(qū)檢索 1,000,000 行慢。接下來,額外的刪除相當(dāng)于額外的寫入,Cassandra 可以輕松擴(kuò)展以處理更多寫入。
鑒于插入和刪除一次又一次地應(yīng)用于相同的行,墓碑很可能在 MemTable 而不是 SSTables 中得到解決,這可以大大減少墓碑的總數(shù)。例如,對于一個給定的物聯(lián)網(wǎng)設(shè)備,即使是頻繁的狀態(tài)更新都命中同一個 MemTable 也只能導(dǎo)致一個墓碑。我們?nèi)匀唤ㄗh監(jiān)控表指標(biāo)以排除任何潛在問題。最后但同樣重要的是,數(shù)據(jù)分布取決于數(shù)據(jù)和應(yīng)用程序特征。在本文的最后一個解決方案中,我們完全控制了數(shù)據(jù)分布。
這種數(shù)據(jù)模型提供了極大的靈活性。通過將每個表的主鍵更改為 PRIMARY KEY((tenant, bucket), device_id) 可以實(shí)現(xiàn)多租戶。更重要的是,可以更改分區(qū)函數(shù)以增加或減少分區(qū)的數(shù)量。檢索較小結(jié)果集的查詢應(yīng)訪問較少數(shù)量的分區(qū)以獲得更好的性能。檢索更大結(jié)果集的查詢應(yīng)訪問更多分區(qū)以更好地分配工作負(fù)載。可以針對不同的狀態(tài)和租戶使用不同的功能以實(shí)現(xiàn)最佳性能。更好的分區(qū)應(yīng)該會帶來更好的性能。
結(jié)論
我們定義了基于狀態(tài)管理最新物聯(lián)網(wǎng)事件的問題,確定了它的挑戰(zhàn),并描述了如何在 Apache Cassandra 中使用五種不同的數(shù)據(jù)模型來解決它。我們陳述了每個數(shù)據(jù)模型的適用性、優(yōu)缺點(diǎn)。我們的最終建議是關(guān)注物化視圖、狀態(tài)分區(qū)表和可自定義的分區(qū)數(shù)據(jù)模型。選擇前兩個是因?yàn)樗鼈兒唵我子谩.?dāng)其他選項(xiàng)用盡時,考慮可定制的分區(qū)以獲得最大的靈活性。最后,開放探索新的可能解決方案,這些解決方案可能會將一些計(jì)算推向應(yīng)用程序或依賴專門的搜索索引和其他技術(shù)。
值得一提的是,這篇博文的動機(jī)是來自 Discord 上 Apache Cassandra 社區(qū)成員的問題。立即加入(Cassandra)戒指團(tuán)契,與社區(qū)成員和專家建立聯(lián)系!






