若是你來自RDBMS的世界,在Cassandra有效地開始建模實體以前,須要一些時間來適應。幫助我解決問題的經驗法則是讓您的查詢定義您須要的實體。app
最近,我將應用程序的持久層從Oracle遷移到Cassandra。雖然遷移一些主要實體至關簡單,但它解決了一些用例,好比支持範圍掃描,這給咱們帶來了一組獨特的挑戰。負載均衡
注意:範圍掃描/查詢基本上是基於時間戳範圍的記錄查找,在它之上有或沒有任何過濾器。dom
在進入實際問題以前,讓咱們先了解一下這個應用程序,我在這裏提到的應用程序處理客戶在咱們的站點上執行的全部事務的全部通訊。咱們平均天天要經過電子郵件、短信和應用程序通知等各類渠道向客戶發送數千萬條信息。分佈式
咱們但願確保在遷移到Cassandra時支持實體上的範圍查詢,緣由有兩個:ide
當咱們使用Oracle時,咱們的團隊已經在一些儀表板上使用了相似的讀取,因此必須確保向後兼容性。性能
例如,在「Y」和「Z」時間之間,在「X」日期爲一個特定事件發送了多少通訊。測試
此外,它還能夠幫助您爲未來可能要執行的任何向下一代持久性解決方案的遷移提供證據。優化
對於這個文章的範圍,讓咱們假設咱們有一個列族定義以下:ui
create table if not exists my_keyspace.message_log ( id uuid, message_id text, event text, event_type text, machine_ip text, created_date date, created_date_timestamp timestamp, primary key ((message_id), id) );this
如今,這能夠幫助您基於message_id查詢消息的詳細信息,由於上面的實體是根據message_id分區的。
可是,若是但願查找在特定時間範圍內着陸的消息,就會出現問題。爲了支持這一點,咱們將引入一個列族,以下所示:
create table if not exists my_keyspace.message_log_dated (
id uuid,
message_id text,
event text,
created_date date,
created_date_timestamp timestamp,
primary key ((created_date), event, created_date_timestamp, message_id, id)
);
如今,這容許咱們執行像下面這樣的查詢,以得到跨時間範圍的數據:
select message_id from my_keyspace.message_log where created_date='2019–12–04'
and event='order-confirmation'
and created_date_timestamp > 1575417601000
and created_date_timestamp < 1575547200000
雖然這可能適用於旨在服務一致和可預測流量的系統,但對於容易受到客戶行爲驅動的意外激增的系統來講,這樣的模型多是一個絕對的噩夢。
因爲上述模型是在created_date列上分區的,所以在給定日期上任何意外的流量激增都將意味着您的全部寫操做都被定向到同一個分區,而且極可能在Cassandra集羣上建立了一個熱點。就集羣性能而言,這基本上意味着三件事:
您可能會冒集羣中的某些節點的風險。
您可能永遠沒法成功讀取此數據,由於查找有超時的風險。
這樣就違反了保持集羣處於健康狀態的黃金法則,即不要讓分區大小增加得太大。(建議將分區大小保持在100 MB左右,但沒有確切的數字。)
咱們作了什麼?咱們嘗試引入一個叫作「一天之窗」(window of the day)的新因素,它只是一個時間插槽(統一),它將一天劃分爲相等的插槽,並將流量分散到多個分區,以下所示:
create table if not exists my_keyspace.message_log_slotted (
id uuid,
message_id text,
event text,
event_type text,
machine_ip text,
created_date date,
slot_id text,
created_date_timestamp timestamp,
primary key ((created_date, slot_id), event, created_date_timestamp, message_id, id)
);
如今,在某一天出現流量激增的狀況下,咱們仍然可以在寫入數據的同時訪問多個分區,並防止任何潛在的熱點。
在本例中,讀取一天的整個數據的查詢是這樣的(假設咱們決定使用6小時的插槽)
select message_id from my_keyspace.message_log where created_date='2019–12–04'
and slot_id in ('slot_00_06', 'slot_06_12', 'slot_12_18', 'slot_18_24')
and created_date_timestamp > 1575417601000
and created_date_timestamp < 1575547200000-- please note this may need some app side filtering to narrow down results to certain events in the entity.
雖然這種方法確實解決了問題,但它只是部分地解決了問題。若是流量激增被限制在一個特定的插槽內,該模型仍然容易出現與第一個模型相同的問題。
您能夠決定將插槽大小減小到更少的小時數甚至分鐘數,但事實是您的模型仍然是易受影響的。
試圖使這種方法適合咱們的生產用例的絕望嘗試,它將須要一個槽的大小約5分鐘,確保即便在最大峯值(當前)咱們可以阻止任何熱點和可以直接交通儘快下一個分區,同時保持當前分區大小可接受範圍。這就意味着咱們須要在12 * 24 = 288個分區中查找一天的數據。
咱們決定後退一步,從新評估咱們的計劃,而不是粗暴地按全部數據Cassandra建模指南行事。咱們的結論是,咱們須要一種可靠的方式向每一個分區發送受控的寫操做,一旦達到限制(每一個分區但願維護的最大存儲空間),咱們的應用程序應該可以將寫操做重定向到一個新的分區。
咱們提出了一種bucketing策略,其中每一個應用程序實例(VMs/computes)爲它持久保存的記錄維護它本身的內存計數器,併爲全部這些寫操做分配一個專用的bucket_id。每次進入流動狀態時,應用程序都會生成一個新的隨機惟一id:
日期已經改變了。
一個新的實體已經被引入。
桶已達到其最大計數。(這意味着在這個特定分區中保存了足夠多的記錄,須要將寫操做移動到一個新的分區。)
這還須要咱們維護一個字典,以維護由全部應用程序實例生成的全部bucket_ids到它們爲之建立的日期和實體的映射。咱們能夠將其持久化到同一個Cassandra密鑰空間中。
在本例中,實體(message_log_date)中支持範圍掃描的行以下所示
created_datebucket_ideventapp_instancecreated_tsmessage_id d1random_unique_id1order-confirmationvm1ts1m1 d1random_unique_id1ship-confirmationvm1ts2m2 d1random_unique_id2ship-confirmationvm2ts3m3 d1random_unique_id3order-confirmationvm3ts4m4 d1random_unique_id1delivery-confirmationvm1ts5m5
而用於維護日期和實體到該範圍各自存儲段之間的映射的字典將是這樣的。
created_dateentity_namecreated_tsapp_instancebucket_id d1message_logts1vm1random_unique_id1 d1message_logts2vm2random_unique_id2 d1success_logts3vm1random_unique_id3 d1failure_logts4vm1random_unique_id4
如今,當你的bucket在某一天溢出時,你可能會看到下面這樣的條目(注意,同一時間內爲同一VM建立的第7行):
created_datebucket_ideventapp_instancecreated_tsmessage_id d1random_unique_id1order-confirmationvm1ts1m1 d1random_unique_id1ship-confirmationvm1ts2m2 d1random_unique_id2ship-confirmationvm2ts3m3 d1random_unique_id3order-confirmationvm3ts4m4 d1random_unique_id1delivery-confirmationvm1ts5m5 d1random_unique_id4delivery-confirmationvm1ts6m1
如今,讓咱們看看如何使用這個模型對給定的實體執行範圍掃描,並使用一些實際的測試數據。
-- view bucket ids.
team@cqlsh> select * from my_keyspace.bucket_id_log where created_date='2019-12-01' and entity_name='MESSAGE_LOG_DATED';created_date | entity_name | created_date_timestamp | id (just here to make pk unique) | bucket_id | created_by
--------------+-------------------+--------------------------+--------------------------------------+--------------------------------------+----------------
2019-12-01 | MESSAGE_LOG_DATED | 2019-12-01 00:00:06+0000 | dc5a7fb9-eeb2-425d-ae9c-4c14f9ab581c | 8751a640-13cd-11ea-bcc0-7f2f66afd78a | 10.65.99.183
2019-12-01 | MESSAGE_LOG_DATED | 2019-12-01 00:00:06+0000 | b61afafe-13a8-4b8d-9014-d249183760e1 | 8751a640-13cd-11ea-bcc0-7f2f66afd78b | 10.65.103.141
2019-12-01 | MESSAGE_LOG_DATED | 2019-12-01 00:00:05+0000 | 5a60bd48-cd97-4912-9cba-d543225e7bfe | 8751a640-13cd-11ea-bcc0-7f2f66afd78c | 10.117.233.105
-- Read bucket ids.
team@cqlsh> select bucket_id from my_keyspace.bucket_id_log where created_date='2019-12-01' and entity_name='MESSAGE_LOG_DATED';bucket_id
--------------------------------------
8751a640-13cd-11ea-bcc0-7f2f66afd78a
8751a640-13cd-11ea-bcc0-7f2f66afd78b
8751a640-13cd-11ea-bcc0-7f2f66afd78c
-- Read message log for the time span.
team@cqlsh> select * from my_keyspace.message_log_dated where created_date='2019-12-01' and bucket_id in (8823ea60-13cd-11ea-b3c9-adfcfeffafbc, 881eba40-13cd-11ea-b0bc-dd0e281ebb5f, 87bbd9c0-13cd-11ea-81d9-81a480b11fcf, 8751a640-13cd-11ea-bcc0-7f2f66afd78c);
created_date | bucket_id | event | created_date_timestamp | message_id | id (part of pk, (surrogate)) | created_by
--------------+---------------------------------------+-----------------------+--------------------------+--------------------------------------+---------------------------------------+----------------
2019-12-01 | 8751a640-13cd-11ea-bcc0-7f2f66afd78a | order-confirmation | 2019-12-01 15:58:00+0000 | 13a07a49-8169-44d1-a7a9-ae8f33ba44c5 | 71c1f81e-cd6e-4ab8-b714-ebf48af32be0 | 10.247.194.101
2019-12-01 | 8751a640-13cd-11ea-bcc0-7f2f66afd78b | ship-confirmation | 2019-12-01 22:21:53+0000 | 3dfd0c23-1a62-45e8-a7b6-d4e14d65149a | 9ea23802-121e-48cc-91ac-711ab261b195 | 10.247.194.102
2019-12-01 | 8751a640-13cd-11ea-bcc0-7f2f66afd78c | delivery-confirmation | 2019-12-01 22:21:17+0000 | 4a24e822-624f-486e-b67c-f25be2f40110 | 9f8ce5ad-69a1-4e1a-8a4f-d31dedba7847 | 10.247.194.103-- now we can easily scan through each message using message ids resulted above for the given time range.
這裏有一個快速查詢,確認每一個bucket都符合分配的最大bucket計數,在個人測試中保持50000。team@cqlsh> select count(*) from my_keyspace.message_log_dated where created_date='2019-12-01' and bucket_id=8751a640-13cd-11ea-bcc0-7f2f66afd78a;
count
-------
50000(1 rows)
下面是這個流程的寫和讀的快速演示:
如今咱們來評估一下這個方法,
優勢:
過濾應該是直接的,你能夠在日期日誌中複製更多的列,這樣你就能夠讀取你想要的配置的消息id。
插入操做簡單快捷。
讀是很快速的。
最重要的是,它解決了咱們在這裏試圖解決的集羣平衡問題。
若是應用程序上的負載均衡器是公正的,它會頗有幫助,但即便它在錯誤/配置改變的狀況下搖擺不定,這種方法仍然會保持穩定。
缺點:
排序(若是須要)將須要在應用程序層進行。
分頁須要在應用程序層上執行。
Cassandra上的範圍掃描解決起來並不簡單,可是隻要有合適的數據模型,就必定能夠解決這個問題。爲了保持集羣正常運行,您的寫操做必須是分佈式的和統一的。
雖然您可能會認爲使用上述建議的解決方案會消耗更多存儲空間,但在Cassandra的世界中這徹底沒問題。你能夠複製數據(而不是以最規格化的形式),若是它有助於優化你的查詢。
使用這種基於「Bucketisation」的方法,您能夠支持範圍掃描,同時避免意外流量激增,而且不會對集羣的運行情況形成任何風險。
最後但並不是最不重要的是,我要對個人導師大喊一聲,感謝他從這個方法的開始到它在生產中發揮做用一直在指導我。