Cassandra + JSON?答案就是Stargate Documents API
JSON已經被開發者在很多場景中頻繁使用,但是其實將Cassandra用於JSON或其他面向文檔的用例並不容易。
為了讓開發者在使用原生的JSON的同時還能享受Cassandra帶來的可靠性和伸縮性,我們開發了Stargate Cassandra Documents API——它使得絕大多數的Cassandra發行版本都能通過一個REST API使用JSON。
如果你像我一樣,當你開始編寫一些新應用程序時,你可能會發現你自己在使用JSON。也許你正在使用Node.js或Python或任何其他動態編程語言(dynamic programming language),這些語言原生的數據格式正巧與JSON類似;或者你正使用從REST API中拉取的數據。
無論是哪種情況,越來越多的情況是所有的新應用程序都會在某個點與JSON匯合。大多數時候這不是個問題,這只是我們如今建構軟件的方式而已。然而有一個問題——Cassandra其實不是特別擅長處理JSON……
如果深入了解一下,你會知道問題並不在於JSON這種數據格式本身,雖然Cassandra並沒有讓JSON變得更易於使用;問題在於大多數程序員在建構程序時使用JSON的方式。
迭代開發意味着計劃經常會改變。比如用戶註冊表格需要一些新的條目,前端開發人員就率先加入了這些條目。當我們使用這個API時,就會有一些額外的數據被返回。歡迎來到這個松耦合(loosely coupled)的世界,所有的一切都是開心和有趣的——直到我的應用程序需要把這些數據發送到數據庫。
其實早期的Cassandra版本要做到這件事很容易,但是隨着這個項目逐漸成熟,像是對企業友好的類SQL查詢語言以及更好的索引等功能被加入之後,意味着我們需要Cassandra數據庫要有一個固定的模式(schema)。逐漸地,將Cassandra用於類似JSON的事情或其他面向文檔的用例變得越來越困難。
進入Stargate(意為「星際之門」)——如果你只需知道一件關於Stargate團隊的事情,那應該就是:我們的個人使命是讓Cassandra變得對於每個開發人員來說都易於使用。想辦法讓Javascript的開發者在使用原生的JSON的同時還能享受Cassandra帶來的可靠性和伸縮性——這是一個我們不能錯過的挑戰。
也正是這個想法催生了Stargate Documents API——它使得絕大多數的Cassandra發行版本(Cassandra 3.11, Cassandra 4.0, and DataStax Enterprise 6.8)都能通過一個REST API使用JSON。
01 Stargate Documents API的特點和設計
當我們剛開始構建這個API的框架時,我們意識到Cassandra完全不像是一個文檔數據庫。
表達一行一行的數據是非常直觀簡單的,但是表達樹狀結構的JSON數據就沒那麼容易了。另外如果還想做到將JSON數據映射到Stargate管理的數據庫表,並保證讀取和寫入的速度還保持在相當快的水準,這就更是複雜了。
基於這些,為了能夠達成目標,我們詳細計划了三個主要的設計考量:基於Cassandra的文檔建模、高效處理讀寫請求,以及合理處理刪除操作。
接下來,這篇文章將介紹我們是如何構想每一個設計以及解決遇到的一些小問題的。
02 基於文檔切分(Document Shredding)在Cassandra中進行文檔建模
我們要決定的第一件事就是管理基於文檔集合(document collection)的數據庫表的模式(schema)。在與幾位Cassandra專家進行了有建設性的討論後,我們決定當用戶創建一個文檔,就會用下面的語句創建一個相應的數據庫表:
create table <name> ( key text, p0 text, … p[N] text, bool_value boolean, txt_value text, dbl_value double, leaf text )
到這裡,我們得要解決長度無上限的數據模型帶來的問題。因為所有有[N]或更少深度的JSON文檔都可以被加入到這個表中,JSON中的每一個值將會在表中存儲為一行。所以如果我想表示一個叫做「x」的含有JSON的文檔:
{"a": { "b": 1 }, "c": 2}
這個文檔將會被「切分」成像這樣的行:
對於包含數組(array)的數據,比如:
{"a": { "b": 1 }, "c": [{"d": 2}]}
就會被拆分成這樣的兩行:
數組元素在存儲時,會在一列中被方括號括起來。
03 高效處理讀寫請求
下一個出現的問題是:想要更新一個文檔,自然而然就要先從數據庫中讀取已有的文檔,看看要對其做些什麼改變,然後再寫入更新的數據。
對大多數數據存儲來說,這個「寫前讀(read-before-write)」的過程是臭名昭著的影響性能和一致性的問題的根源。因此,我們決心要不惜一切避免任何「寫前讀」的操作。
有一個具體的實現細節很有意思——當你向文檔寫入一些數據時,這個寫入操作其實只是一個包含了一些插入操作和刪除操作的簡單的批處理。在某些情況下,這會導致文檔在數據庫中對應的行針對同一個JSON數據域,可能會顯示出兩種不同的狀態。
然後,當這些行都被讀取出來之後,Stargate的Cassandra Documents API就會選取寫入時間戳比較新的數據,從而調和衝突的信息(這和Cassandra本身的原理很類似)。
這讓我們的寫入操作變得很快,同時不必在讀取操作方面犧牲太多——因為前面所說的這種需要衝突調和的情況並不多見,即使出現,也會很快被解決。對於我們基礎的讀寫操作,這也奠定了非常重要的核心原則:
-
每一個向一個單獨的文檔所做的寫入操作,都是一個單獨的、含有多個語句的批處理。
-
每一個向一個單獨的文檔所做的讀取操作,都是一個單獨的SELECT語句。
讀操作和寫操作都準備就緒了,那刪除操作呢?
04 合理處理刪除
由於Cassandra數據庫本身的分佈式屬性,在Cassandra中做刪除操作其實與插入操作很類似。不過刪除操作所做的,是在特定的寫入時間通過寫入「tombstone(墓碑)」來標誌一行數據的死亡。
入土為安吧……等等,事情其實還沒結束——
Cassandra會周期性地做壓實(compaction)操作(這個頻率取決於你的壓實策略和/或集群負載),從而刪除墓碑並釋放壓力。所以避免讓Cassandra過載的唯一方法就是確保刪除操作的頻率足夠的低。
由於數組的存在,刪除操作給Stargate的Cassandra Documents API帶來了一個問題。現在讓我們來談談這點。
想像一下,如果你有一個行,其中的某個單元是一個長度為十萬個元素的數組。然後如果你進行一個更新操作(通過HTTP中的PUT方法)並決定給這個單元賦值為一個新的數組。結果就是整個十萬行的數據都要被刪除,也就是說整整十萬個墓碑要被寫入系統。
這麼多墓碑在一次操作中被寫入,其數量相當之大。如果你再多這麼操作幾次,Cassandra很可能會變得非常之慢。所以,我們還需要對每個表的數據結構再做一次大的調整。
我們前面提到過,存在數據庫中的數組訪問路徑(array path)是帶方括號的。比如數組序列為0的元素會被存儲為[0]。這將意味着像下面這樣刪除100,000個元素:
DELETE FROM <name> where p0 in ('[0]', '[1]', '[2]', …, '[99999]')
這就導致了100,000個墓碑將被寫入。
相比這麼做,我們決定向所有的數組元素的開頭都充填多個0——也就是說索引序號為0的元素會被記作[000000],而索引序號為99999的元素會被表示記作[099999]。通過這種方式,我們可以將刪除語句改成這樣:
DELETE FROM <name> where p0 >= '[000000]' and p0 <= '[999999]'
相比100,000個單元的墓碑,這種方式會使得只有一個所謂的「區間」墓碑(range tombstone)會被寫入(提示:在Cassandra中,大於號和小於號可以依照字典順序作用於字符串類型的數據)。這也將數組長度限制放寬至一百萬個元素,這可真是相當巧妙!
下方的時間序列圖顯示了在你以每周一次的頻率做壓實操作的前提下,舊方法和新方法的效果對比:
相比100,000個單元的墓碑,這種方式會使得只有一個所謂的「區間」墓碑(range tombstone)會被寫入(提示:在Cassandra中,大於號和小於號可以依照字典順序作用於字符串類型的數據)。這也將數組長度限制放寬至一百萬個元素,這可真是相當巧妙!
下方的時間序列圖顯示了在你以每周一次的頻率做壓實操作的前提下,舊方法和新方法的效果對比:
05 簡單了解此API的性能
⚠️在開始這個部分之前,我們想先提一下:雖然基準測試是非常好的工具,但並非能絕對說明一個系統在自然條件和真實負載下的表現。另外,我們也還沒在同一硬件上,將擁有此API加成的Cassandra與其它文檔數據庫進行對比……是的,還沒有。好吧,讓我們現在就來看看它的性能!
為了測試我們的Cassandra Documents API是否足夠的快,我們用一個單獨的Cassandra存儲節點和一個單獨的Stargate節點進行了一次基準測試(Stargate是包含了Cassandra Documents API的API)。
然後我們進行了兩個不同的基準測試,一個用HTTP GET重複地在一個文檔中隨機獲取路徑,另一個用HTTP POST重複地創建新的文檔。這兩個操作都分別進行了100,000次,下面的圖中展示了這次測試的結果。
簡單來說,由於現在並沒有可以用於對比的基準數據,這次基準測試中分為這樣三種情況:一次只有一個用戶、稍微多一些並發操作(一次10個用戶)、更多的並發操作(一次100個用戶)。
需注意,此處我們在後端只有一個節點,更多的並發操作會引起性能的退化(degradation in performance)。如果想要承載更高程度的並發請求,你應該使用多個節點。
下面的是讀操作的測試結果:
⚠️在開始這個部分之前,我們想先提一下:雖然基準測試是非常好的工具,但並非能絕對說明一個系統在自然條件和真實負載下的表現。另外,我們也還沒在同一硬件上,將擁有此API加成的Cassandra與其它文檔數據庫進行對比……是的,還沒有。好吧,讓我們現在就來看看它的性能!
為了測試我們的Cassandra Documents API是否足夠的快,我們用一個單獨的Cassandra存儲節點和一個單獨的Stargate節點進行了一次基準測試(Stargate是包含了Cassandra Documents API的API)。
然後我們進行了兩個不同的基準測試,一個用HTTP GET重複地在一個文檔中隨機獲取路徑,另一個用HTTP POST重複地創建新的文檔。這兩個操作都分別進行了100,000次,下面的圖中展示了這次測試的結果。
簡單來說,由於現在並沒有可以用於對比的基準數據,這次基準測試中分為這樣三種情況:一次只有一個用戶、稍微多一些並發操作(一次10個用戶)、更多的並發操作(一次100個用戶)。
需注意,此處我們在後端只有一個節點,更多的並發操作會引起性能的退化(degradation in performance)。如果想要承載更高程度的並發請求,你應該使用多個節點。
下面的是讀操作的測試結果:
下面的是寫操作的測試結果:
從上面的結果中,我們可以看到在我們設置的一定程度上的並發操作下,Stargate的Cassandra Documents API表現相當不錯。
06 結束語
我們希望你享受這篇對於Stargate Cassandra Documents API的快速介紹。如果你對使用這個API有興趣,不妨點擊文末「閱讀原文」前往Stargate.io查看更多相關信息,看看如何將這個API用在你自己的Cassandra系統中。
來加入我們的Discord Community,你可以在此獲取Stargate的最新動態,並且最先使用Stargate的新功能
//discord.com/invite/5gY8GDB
如果你有任何問題或是想要提交任何代碼,來看看我們的GitHub倉庫吧
//github.com/stargate/stargate
Stargate的Cassandra Documents API正在被積極開發中,我們盼望着很快帶給你有關更多提升改進的新消息!